From d8b8adaef4a096fdb146e388138cc04fa2472abf Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Mon, 14 Sep 2026 10:37:55 -0400 Subject: [PATCH] forgetest: the setup suite, and the sensors check asks nothing The commission.* tests are setup.* (subsystem "setup"): suite/setup.py, setup_dark.py, setup_sheet.py, and the host tests test_setup_*.py. The coverage maps name src/setup.* in place of src/commission.*, the record is setup.json, and the bench seed in forgetest.init creates /run/forgefirm/setup-override. setup.check-sensors follows the check as it is now: no question is asked and no setting is written. The test reads the settings before and after and fails on any change, and fails at once if the check opens a prompt. The host test drives the fake daemon with no prompt and proves both outcomes. Proof: test_setup_dark.py, test_setup_sheet.py, and test_setup_suite.py pass (46 tests). The coverage lint names src/commission.c and .h as uncovered until the forgectrl pin moves to the revision that carries the rename. --- forgetest/forgetest.init | 4 +- forgetest/forgetest/baseline.py | 2 +- forgetest/forgetest/suite/__init__.py | 6 +- forgetest/forgetest/suite/camera.py | 2 +- forgetest/forgetest/suite/cooling.py | 2 +- forgetest/forgetest/suite/forgectrl.py | 2 +- .../suite/{commission.py => setup.py} | 102 +++++------ .../{commission_dark.py => setup_dark.py} | 72 ++++---- .../{commission_sheet.py => setup_sheet.py} | 12 +- ..._commission_dark.py => test_setup_dark.py} | 64 ++++--- ...ommission_sheet.py => test_setup_sheet.py} | 28 +-- ...ommission_suite.py => test_setup_suite.py} | 160 +++++++++--------- 12 files changed, 243 insertions(+), 213 deletions(-) rename forgetest/forgetest/suite/{commission.py => setup.py} (95%) rename forgetest/forgetest/suite/{commission_dark.py => setup_dark.py} (87%) rename forgetest/forgetest/suite/{commission_sheet.py => setup_sheet.py} (96%) rename forgetest/tests/{test_commission_dark.py => test_setup_dark.py} (62%) rename forgetest/tests/{test_commission_sheet.py => test_setup_sheet.py} (95%) rename forgetest/tests/{test_commission_suite.py => test_setup_suite.py} (73%) diff --git a/forgetest/forgetest.init b/forgetest/forgetest.init index 76d7f7f..ab6ca6a 100644 --- a/forgetest/forgetest.init +++ b/forgetest/forgetest.init @@ -22,13 +22,13 @@ case "$1" in start) echo "Starting forgetest" mkdir -p "$DATA" - # The bench seed: the commissioning override lifts the wizard part of + # The bench seed: the setup override lifts the wizard part of # the controller gate until the next reboot (/run is tmpfs), so a # campaign runs on a machine whose required wizards are not complete. # Nothing lifts the agreements or the account: the bench accepts them # once, and that stays in /data. mkdir -p /run/forgefirm - : > /run/forgefirm/commissioning-override + : > /run/forgefirm/setup-override start-stop-daemon -S -q -p $PIDFILE -m -b -x /bin/sh -- -c \ "exec /usr/bin/python3 -m forgetest >> $LOG 2>&1" ;; diff --git a/forgetest/forgetest/baseline.py b/forgetest/forgetest/baseline.py index 82b4132..5daad57 100644 --- a/forgetest/forgetest/baseline.py +++ b/forgetest/forgetest/baseline.py @@ -321,7 +321,7 @@ class Baseline: def wait_settled(self, timeout=SETTLE_S, unreachable_s=10): """Block until forgectrl reports a settled supervisor: motion verified (the probe passed), motion-fault (the ladder exhausted), - standby (the manual stop lever), or gated (the commissioning gate + standby (the manual stop lever), or gated (the setup gate is closed: no controller spawns until it opens). Gives up after unreachable_s without an answer. Returns the last /mode body (None if unreachable).""" diff --git a/forgetest/forgetest/suite/__init__.py b/forgetest/forgetest/suite/__init__.py index 6117315..7aaacb4 100644 --- a/forgetest/forgetest/suite/__init__.py +++ b/forgetest/forgetest/suite/__init__.py @@ -4,9 +4,9 @@ order. Each module registers its tests with @catalog.test.""" from . import image # noqa: F401,E402 from . import kernel # noqa: F401,E402 from . import forgectrl # noqa: F401,E402 -from . import commission # noqa: F401,E402 -from . import commission_dark # noqa: F401,E402 -from . import commission_sheet # noqa: F401,E402 +from . import setup # noqa: F401,E402 +from . import setup_dark # noqa: F401,E402 +from . import setup_sheet # noqa: F401,E402 from . import logs # noqa: F401,E402 from . import motion # noqa: F401,E402 from . import cooling # noqa: F401,E402 diff --git a/forgetest/forgetest/suite/camera.py b/forgetest/forgetest/suite/camera.py index cf4e4ac..d2eba7a 100644 --- a/forgetest/forgetest/suite/camera.py +++ b/forgetest/forgetest/suite/camera.py @@ -396,7 +396,7 @@ def lid_privacy(ctx): "authorizes a write; a rotation from the panel route stops the old key and " "the new one reads. The setting is put back after.") def key_read(ctx): - from .commission import request + from .setup import request from .forgectrl import lan_ip fc = ctx.forgectrl ev = ctx.evidence diff --git a/forgetest/forgetest/suite/cooling.py b/forgetest/forgetest/suite/cooling.py index 86d434e..cfecd5f 100644 --- a/forgetest/forgetest/suite/cooling.py +++ b/forgetest/forgetest/suite/cooling.py @@ -891,7 +891,7 @@ CRASH_GATES = ["crash_abort", "crash_x_alert", "crash_y_alert"] "read as the three crash gates off by value (gates_off names them) with the " "session OK; a threshold past the register range is refused; restored, the " "gates read ok. The tiers themselves (BUMP, CRASH) trip only on a physical " - "knock inside an armed window and are commissioned at the bench, not here.") + "knock inside an armed window and are proven at the bench, not here.") def crash_watch_plumbing(ctx): fc = ctx.forgectrl ev = ctx.evidence diff --git a/forgetest/forgetest/suite/forgectrl.py b/forgetest/forgetest/suite/forgectrl.py index b065651..f5f84f5 100644 --- a/forgetest/forgetest/suite/forgectrl.py +++ b/forgetest/forgetest/suite/forgectrl.py @@ -152,7 +152,7 @@ def auth(ctx): # HTTPS the route itself refuses the peer (403 loopback only). Neither # request follows the redirect, and the self-signed certificate is # not verified. - from .commission import request, decode + from .setup import request, decode ip = lan_ip() ev["lan_ip"] = ip ctx.check(ip, "cannot determine the board's LAN address") diff --git a/forgetest/forgetest/suite/commission.py b/forgetest/forgetest/suite/setup.py similarity index 95% rename from forgetest/forgetest/suite/commission.py rename to forgetest/forgetest/suite/setup.py index 96e3e11..507f2f7 100644 --- a/forgetest/forgetest/suite/commission.py +++ b/forgetest/forgetest/suite/setup.py @@ -1,9 +1,9 @@ -"""commission.* - the first-run commissioning: the record and the +"""setup.* - the first-run setup: the record and the controller gate, the advisories, the account and its login, the HTTPS boundary, SSH, the cloud switch, the machine's name, and the factory return. -The daemon reads the commissioning record (commissioning.json in the +The daemon reads the setup record (setup.json in the data directory) and the account record (users) once, at its start, and keeps them in memory; only its own routes write them. A test that needs a different record installs the file under a forgectrl restart (a @@ -34,13 +34,13 @@ from ..catalog import test from .. import hw from .forgectrl import lan_ip -RECORD = "commissioning.json" +RECORD = "setup.json" USERS = "users" -OVERRIDE = "commissioning-override" +OVERRIDE = "setup-override" SSH_FLAG = "ssh-enabled" TLS_BASE = "https://127.0.0.1" # FORGECTRL_TLS_URL overrides (host tests) -# The image's required wizards, all at version 1 (commission.c). +# The image's required wizards, all at version 1 (setup.c). REQUIRED_WIZARDS = ("advisories", "account", "preferences", "machine", "cloud") ADVISORY_DOCS = ("safety-and-risk", "licenses", "privacy", "cloud-service") SAFETY_PHRASE = "I UNDERSTAND" @@ -176,8 +176,8 @@ def record_without_wizards(record): def complete_record(status, base=None, account_name="bench"): - """A complete commissioning record for a machine that must count as - commissioned: every advisory at the hash GET /wiz reports, the + """A complete setup record for a machine that must count as + set up: every advisory at the hash GET /wiz reports, the press recorded, an account (the one of `base` when it has one), every wizard of the catalog at its version, and the run complete. `status` is a GET /wiz body; `base` is an existing record to keep @@ -352,12 +352,12 @@ def gate_is_open(): # -------------------------------------------------------------- the gate -_GATE_COVERS = [("forgectrl", "src/commission.*"), ("forgectrl", "src/super.c"), +_GATE_COVERS = [("forgectrl", "src/setup.*"), ("forgectrl", "src/super.c"), ("forgectrl", "src/main.c"), ("forgectrl", "src/paths.h")] -@test("commission.gate-blocks-controllers", title="The commissioning gate spawns no controller", - subsystem="commission", kind="auto", hardware="takeover", mode="grbl", est_min=5, +@test("setup.gate-blocks-controllers", title="The setup gate spawns no controller", + subsystem="setup", kind="auto", hardware="takeover", mode="grbl", est_min=5, covers=_GATE_COVERS, requires=["forgectrl.auth"], precheck=gate_is_open, description="With the override removed and a record that lacks every required wizard " "(installed under a forgectrl restart, the consent and the account kept), " @@ -374,11 +374,11 @@ def gate_blocks_controllers(ctx): ctx.log("before: gate %s, override %s, required %s", before.get("gate"), before.get("override"), before.get("required")) raw = read_file(record_path()) - ctx.check(raw, "no commissioning record at %s", record_path()) + ctx.check(raw, "no setup record at %s", record_path()) try: record = json.loads(raw.decode("utf-8")) except ValueError as e: - ctx.fail("the commissioning record is not JSON: %s", e) + ctx.fail("the setup record is not JSON: %s", e) lacking = json.dumps(record_without_wizards(record), indent=1, sort_keys=True).encode() + b"\n" had_override = override_present() ev["override_before"] = had_override @@ -391,7 +391,7 @@ def gate_blocks_controllers(ctx): ctx.log("/mode after the restart: %s (after %s s)", m, took) ctx.check(m.get("controller") == "gated" and m.get("gated") is True, "the supervisor did not report the gate: %s", m) - ctx.check("commissioning required" in (m.get("why") or ""), + ctx.check("setup required" in (m.get("why") or ""), "the reason does not name the missing wizards: %r", m.get("why")) w = wiz(fc) ev["gated_wiz"] = wiz_summary(w) @@ -439,11 +439,11 @@ def gate_blocks_controllers(ctx): ctx.log("restored: gate %s, controller %s", after.get("gate"), m.get("controller")) -@test("commission.override-until-reboot", title="The override stands until the next reboot", - subsystem="commission", kind="auto", est_min=1, +@test("setup.override-until-reboot", title="The override stands until the next reboot", + subsystem="setup", kind="auto", est_min=1, covers=_GATE_COVERS, requires=["forgectrl.auth"], description="The bench seed: the dev image's forgetest init script writes " - "/run/forgefirm/commissioning-override at boot. With it present, GET /wiz " + "/run/forgefirm/setup-override at boot. With it present, GET /wiz " "reports the override and an open gate, the reason names the wizards it lifts " "when any is required, and GET /mode reports the controller running and not " "gated. The file lives on tmpfs (/proc/mounts), so a reboot removes it by " @@ -486,9 +486,9 @@ def override_until_reboot(ctx): # ------------------------------------------------------------ advisories -@test("commission.advisories-rehash", title="An advisory is accepted at its current hash only", - subsystem="commission", kind="auto", hardware="takeover", est_min=3, - covers=[("forgectrl", "src/advisories.*"), ("forgectrl", "src/commission.*"), +@test("setup.advisories-rehash", title="An advisory is accepted at its current hash only", + subsystem="setup", kind="auto", hardware="takeover", est_min=3, + covers=[("forgectrl", "src/advisories.*"), ("forgectrl", "src/setup.*"), ("forgectrl", "src/wiz.*"), ("forgectrl", "src/sha256.*"), ("forgectrl", "src/main.c"), ("forgectrl", "src/ui/md.js"), ("forgectrl", "src/ui/embed_docs.cmake")], requires=["forgectrl.auth"], @@ -504,7 +504,7 @@ def advisories_rehash(ctx): fc = ctx.forgectrl ev = ctx.evidence raw = read_file(record_path()) - ctx.check(raw, "no commissioning record at %s", record_path()) + ctx.check(raw, "no setup record at %s", record_path()) before = wiz(fc) ev["before"] = wiz_summary(before) docs = {d.get("id"): d for d in before.get("documents") or []} @@ -579,7 +579,7 @@ def advisories_rehash(ctx): # -------------------------------------------------------------- the login -@test("commission.account-login", title="The panel login over HTTPS", subsystem="commission", +@test("setup.account-login", title="The panel login over HTTPS", subsystem="setup", kind="auto", est_min=2, covers=[("forgectrl", "src/auth.*"), ("forgectrl", "src/session.*"), ("forgectrl", "src/users.*"), ("forgectrl", "src/tls.*"), ("forgectrl", "src/peer.*"), ("forgectrl", "src/main.c"), @@ -702,7 +702,7 @@ def login_checks(ctx, name, pw): # ------------------------------------------------------- HTTPS boundary -@test("commission.https-only-writes", title="Writes from the LAN go to HTTPS", subsystem="commission", +@test("setup.https-only-writes", title="Writes from the LAN go to HTTPS", subsystem="setup", kind="auto", est_min=1, covers=[("forgectrl", "src/auth.*"), ("forgectrl", "src/tls.*"), ("forgectrl", "src/peer.*"), ("forgectrl", "src/main.c")], @@ -802,8 +802,8 @@ def restore_ssh(fc, before): write_file(flag, b"" if before.get("enabled") else None) -@test("commission.ssh-until-reboot", title="SSH is switched from the panel until the next reboot", - subsystem="commission", kind="auto", est_min=1, +@test("setup.ssh-until-reboot", title="SSH is switched from the panel until the next reboot", + subsystem="setup", kind="auto", est_min=1, covers=[("forgectrl", "src/main.c"), ("forgectrl", "src/paths.h")], requires=["forgectrl.auth"], description="GET /system/ssh reports enabled, running, and dev_image, and agrees with the " @@ -888,8 +888,8 @@ def ssh_until_reboot(ctx): # ---------------------------------------------------------------- cloud -@test("commission.cloud-disabled-surface", title="Nothing points at the cloud while it is off", - subsystem="commission", kind="auto", mode="grbl", est_min=1, +@test("setup.cloud-disabled-surface", title="Nothing points at the cloud while it is off", + subsystem="setup", kind="auto", mode="grbl", est_min=1, covers=[("forgectrl", "src/main.c"), ("forgectrl", "src/super.c"), ("forgectrl", "src/settings.*"), ("forgectrl", "src/wiz.c"), ("forgectrl", "src/hooks.h")], requires=["forgectrl.settings-bounds"], @@ -971,8 +971,8 @@ def cloud_disabled_surface(ctx): # ---------------------------------------------------------- the operator -@test("commission.factory-return", title="The return to the factory firmware is guarded", - subsystem="commission", kind="auto", est_min=1, +@test("setup.factory-return", title="The return to the factory firmware is guarded", + subsystem="setup", kind="auto", est_min=1, covers=[("forgectrl", "src/update.*"), ("forgectrl", "src/main.c"), ("forgectrl", "src/ui/wizard.js")], requires=["update.slots-and-signature"], description="The return itself reboots the machine as a Glowforge, so it never runs from the " @@ -1034,8 +1034,8 @@ def cmdlines(): return out -@test("commission.machine-name", title="The machine names itself from its MAC address", - subsystem="commission", kind="auto", est_min=1, +@test("setup.machine-name", title="The machine names itself from its MAC address", + subsystem="setup", kind="auto", est_min=1, requires=["forgectrl.auth"], description="The machine calls itself forgefirm-, where xxxx is the last four hex " "digits of its WiFi MAC address, so two machines on one network answer to " @@ -1213,12 +1213,12 @@ def finished_first_run(ctx): "the controller did not come back after the setup: %s", m) -_FIRST_RUN_REQUIRES = ["commission.gate-blocks-controllers", "commission.advisories-rehash"] +_FIRST_RUN_REQUIRES = ["setup.gate-blocks-controllers", "setup.advisories-rehash"] -@test("commission.first-run-flow", title="The first run, driven as the page drives it", subsystem="commission", +@test("setup.first-run-flow", title="The first run, driven as the page drives it", subsystem="setup", kind="operator", hardware="takeover", est_min=3, - covers=[("forgectrl", "src/wiz.*"), ("forgectrl", "src/commission.*"), ("forgectrl", "src/users.*"), + covers=[("forgectrl", "src/wiz.*"), ("forgectrl", "src/setup.*"), ("forgectrl", "src/users.*"), ("forgectrl", "src/session.*"), ("forgectrl", "src/button.*"), ("forgectrl", "src/led.*"), ("forgectrl", "src/sheetid.*"), ("forgectrl", "src/advisories.*"), ("forgectrl", "src/hooks.h"), ("forgectrl", "src/main.c")], @@ -1309,11 +1309,11 @@ def first_run_flow(ctx): ctx.log("the run completed: gate open, the controller back") -@test("commission.first-run-page", title="The first-run page, walked once", subsystem="commission", +@test("setup.first-run-page", title="The first-run page, walked once", subsystem="setup", kind="operator", hardware="takeover", est_min=6, covers=[("forgectrl", "src/ui/wizard.*"), ("forgectrl", "src/ui/forms.js"), ("forgectrl", "src/ui/md.js"), ("forgectrl", "src/ui/login.*"), ("forgectrl", "src/ui/embed_docs.cmake"), ("forgectrl", "src/tls.*")], - requires=_FIRST_RUN_REQUIRES + ["commission.first-run-flow"], + requires=_FIRST_RUN_REQUIRES + ["setup.first-run-flow"], actions=["button"], hands=["workstation"], steps=["From the workstation open https://
/ and accept the self-signed certificate.", "Click through the setup: scroll each document to its end and accept it (type I " @@ -1328,7 +1328,7 @@ def first_run_flow(ctx): "clicks through (the documents accepted, the press step with the LEDs " "breathing teal, the account created, the run complete, the gate open, the " "controller back). The operator confirms the panel opened. Everything the run " - "wrote is restored as commission.first-run-flow restores it.") + "wrote is restored as setup.first-run-flow restores it.") def first_run_page(ctx): fc = ctx.forgectrl ev = ctx.evidence @@ -1364,8 +1364,8 @@ def first_run_page(ctx): ctx.confirm("Did the control panel open after Finish?") -@test("commission.cert-page", title="The certificate is checkable before the warning", - subsystem="commission", kind="auto", est_min=1, +@test("setup.cert-page", title="The certificate is checkable before the warning", + subsystem="setup", kind="auto", est_min=1, covers=[("forgectrl", "src/tls.*"), ("forgectrl", "src/main.c"), ("forgectrl", "src/ui/wizard.html"), ("forgectrl", "src/ui/wizard.js")], requires=["forgectrl.auth"], @@ -1423,9 +1423,9 @@ def flag_of(w, level, wid): return None -@test("commission.what-changed", title="A replaced part asks for its checks again", - subsystem="commission", kind="auto", est_min=2, hardware="takeover", - covers=[("forgectrl", "src/commission.*"), ("forgectrl", "src/wiz.*"), ("forgectrl", "src/main.c"), +@test("setup.what-changed", title="A replaced part asks for its checks again", + subsystem="setup", kind="auto", est_min=2, hardware="takeover", + covers=[("forgectrl", "src/setup.*"), ("forgectrl", "src/wiz.*"), ("forgectrl", "src/main.c"), ("forgectrl", "src/ui/panel.js"), ("forgectrl", "src/ui/index.html"), ("forgectrl", "src/ui/help.js")], requires=["forgectrl.auth"], @@ -1493,9 +1493,9 @@ def what_changed(ctx): ctx.check(bool(mode(fc).get("gated")) == gated_before, "the gate does not read as before") -@test("commission.record-export", title="The record: JSON, the printable page, and the log bundle", - subsystem="commission", kind="auto", est_min=2, - covers=[("forgectrl", "src/commission.*"), ("forgectrl", "src/recordhtml.*"), ("forgectrl", "src/wiz.*"), +@test("setup.record-export", title="The record: JSON, the printable page, and the log bundle", + subsystem="setup", kind="auto", est_min=2, + covers=[("forgectrl", "src/setup.*"), ("forgectrl", "src/recordhtml.*"), ("forgectrl", "src/wiz.*"), ("forgectrl", "src/logs.*"), ("forgectrl", "src/main.c"), ("forgectrl", "src/ui/index.html"), ("forgectrl", "src/ui/panel.js"), ("forgectrl", "src/ui/wizard.html")], requires=["forgectrl.auth", "logs.tree-tail-export"], @@ -1504,7 +1504,7 @@ def what_changed(ctx): "named after the sheet id; GET /wiz/record.html is a page with no script that " "carries the sheet id and every completed wizard; without the token and without " "a login both are refused (403); and the sanitized log export carries " - "system/commissioning.json, parseable, with the same sheet id and no panel token.") + "system/setup.json, parseable, with the same sheet id and no panel token.") def record_export(ctx): import gzip import io @@ -1544,7 +1544,7 @@ def record_export(ctx): ctx.check(st == 200 and "text/html" in (hdrs.get("content-type") or ""), "GET /wiz/record.html -> %s %s", st, hdrs.get("content-type")) ctx.check("" + titles[d]) not in page] ev["missing_on_page"] = missing @@ -1565,11 +1565,11 @@ def record_export(ctx): try: tf = tarfile.open(fileobj=io.BytesIO(gzip.decompress(data))) for m in tf.getmembers(): - if m.isfile() and m.name.endswith("system/commissioning.json"): + if m.isfile() and m.name.endswith("system/setup.json"): member = tf.extractfile(m).read() except (OSError, tarfile.TarError, EOFError) as e: ctx.fail("export is not a readable tar.gz: %s", e) - ctx.check(member is not None, "the bundle lacks system/commissioning.json") + ctx.check(member is not None, "the bundle lacks system/setup.json") if member is not None: try: inside = json.loads(member.decode("utf-8")) @@ -1587,12 +1587,12 @@ def record_export(ctx): ctx.log("record %d wizards; page %d bytes; bundle carries the record", len(done), ev["page_bytes"]) -@test("commission.mirror", title="One browser drives a check; a second one follows", - subsystem="commission", kind="auto", est_min=2, hardware="takeover", +@test("setup.mirror", title="One browser drives a check; a second one follows", + subsystem="setup", kind="auto", est_min=2, hardware="takeover", covers=[("forgectrl", "src/wizdark.*"), ("forgectrl", "src/wizcalc.*"), ("forgectrl", "src/wiz.*"), ("forgectrl", "src/session.*"), ("forgectrl", "src/ui/wizard.js"), ("forgectrl", "src/ui/wizard.html"), ("forgectrl", "src/ui/wizard.css")], - requires=["forgectrl.auth", "commission.account-login", "commission.check-sensors"], + requires=["forgectrl.auth", "setup.account-login", "setup.check-sensors"], description="With a temporary account (made and removed as the login test does) and two " "login sessions: the first session starts the sensors check; GET /wiz/dark " "reports the run as owned and its own to the first session, not to the second, " diff --git a/forgetest/forgetest/suite/commission_dark.py b/forgetest/forgetest/suite/setup_dark.py similarity index 87% rename from forgetest/forgetest/suite/commission_dark.py rename to forgetest/forgetest/suite/setup_dark.py index 8f1a809..43d21fd 100644 --- a/forgetest/forgetest/suite/commission_dark.py +++ b/forgetest/forgetest/suite/setup_dark.py @@ -1,4 +1,4 @@ -"""commission.check-* - the setup's checks (the dark wizards), driven the +"""setup.check-* - the setup's checks (the dark wizards), driven the way the page drives them: POST /wiz//start, GET /wiz/dark polled, the prompts answered from here (the bench fixture works the lid and the button; a confirmation is answered yes once the snapshot exists), the @@ -12,7 +12,7 @@ import time from ..catalog import test from .. import hw from ..baseline import read_position -from .commission import wiz, Restore # noqa: F401 - Restore is re-exported for the sheet +from .setup import wiz, Restore # noqa: F401 - Restore is re-exported for the sheet POLL_S = 1.0 # What the machine's own press prompt says, used when it sends no text. @@ -22,7 +22,7 @@ PRESS_TEXT = "The button is lit white. Press it now: the laser fires after your # (74) for the first seconds and settles near 630. PURGE_SPINUP_S = 30 DARK_COVERS = [("forgectrl", "src/wizdark.*"), ("forgectrl", "src/wizcalc.*"), - ("forgectrl", "src/wiz.*"), ("forgectrl", "src/commission.*"), + ("forgectrl", "src/wiz.*"), ("forgectrl", "src/setup.*"), ("forgectrl", "src/main.c"), ("forgectrl", "src/ui/wizard.*")] @@ -113,8 +113,8 @@ def run_check(ctx, wid, on_prompt, timeout_s): return last -@test("commission.check-switches", title="The switches check follows the lid and the button", - subsystem="commission", kind="operator", est_min=3, +@test("setup.check-switches", title="The switches check follows the lid and the button", + subsystem="setup", kind="operator", est_min=3, covers=DARK_COVERS + [("forgectrl", "src/status.c")], requires=["forgectrl.auth"], actions=["lid", "button"], description="POST /wiz/switches/start; the check asks for the lid to open and close and for " @@ -148,30 +148,40 @@ def check_switches(ctx): ctx.log("switches: %s", json.dumps(r)) -@test("commission.check-sensors", title="The sensors check reads a plausible machine at rest", - subsystem="commission", kind="auto", est_min=2, +@test("setup.check-sensors", title="The sensors check reads a plausible machine at rest", + subsystem="setup", kind="auto", est_min=2, covers=DARK_COVERS + [("forgectrl", "src/status.c"), ("forgectrl", "src/accel.c"), ("forgectrl", "src/cool.c")], requires=["forgectrl.auth"], - description="POST /wiz/sensors/start; ten seconds of readings, then the room-temperature " - "prompt, answered Skip so no offset is written. The result carries both coolant " - "temperatures, the chassis and SoC, the lid IR maxima, the accelerometer event " - "count, the supply power-good, the HV current, and the idle fan speeds; the " - "record carries sensors at version 1 and cool_temp_offset_c reads as before.") + description="POST /wiz/sensors/start; ten seconds of readings and no question asked. The " + "result carries both coolant temperatures, the chassis and SoC, the lid IR " + "maxima, the accelerometer event count, the supply power-good, the HV current, " + "and the idle fan speeds; the record carries sensors at version 1 and the " + "settings read as before (the check writes none).") def check_sensors(ctx): - with Restore(ctx, ["cool_temp_offset_c"]): - last = run_check(ctx, "sensors", lambda p: "Skip" if p.get("id") == "room-temp" else None, 120) - r = last.get("result") or {} - for k in ("coolant_down_c", "coolant_up_c", "lid_ir_max", "laser_pgood", "hv_current_max", - "exhaust_rpm_idle", "intake_rpm_idle"): - ctx.check(k in r, "the result lacks %s: %s", k, r) - ctx.check(r.get("laser_pgood") == 1, "power-good read %s", r.get("laser_pgood")) - ctx.log("sensors: coolant %s/%s C, IR %s, HV %s", r.get("coolant_down_c"), r.get("coolant_up_c"), - r.get("lid_ir_max"), r.get("hv_current_max")) + fc = ctx.forgectrl + ev = ctx.evidence + before = fc.settings() or {} + ev["settings_before"] = dict(before) + + def on_prompt(p): + ctx.fail("the sensors check asked a question: %s", p) + last = run_check(ctx, "sensors", on_prompt, 120) + r = last.get("result") or {} + for k in ("coolant_down_c", "coolant_up_c", "lid_ir_max", "laser_pgood", "hv_current_max", + "exhaust_rpm_idle", "intake_rpm_idle"): + ctx.check(k in r, "the result lacks %s: %s", k, r) + ctx.check(r.get("laser_pgood") == 1, "power-good read %s", r.get("laser_pgood")) + after = fc.settings() or {} + changed = {k: (before.get(k), after.get(k)) for k in set(before) | set(after) + if before.get(k) != after.get(k)} + ctx.check(not changed, "the sensors check wrote a setting: %s", changed) + ctx.log("sensors: coolant %s/%s C, IR %s, HV %s", r.get("coolant_down_c"), r.get("coolant_up_c"), + r.get("lid_ir_max"), r.get("hv_current_max")) -@test("commission.check-airflow", title="The airflow check measures the fans and sets the floors", - subsystem="commission", kind="auto", hardware="takeover", est_min=3, +@test("setup.check-airflow", title="The airflow check measures the fans and sets the floors", + subsystem="setup", kind="auto", hardware="takeover", est_min=3, covers=DARK_COVERS + [("forgectrl", "src/cool.c"), ("forgectrl", "src/airflow.*"), ("forgectrl", "src/gates.c"), ("forgectrl", "src/super.c")], requires=["forgectrl.auth", "cooling.fan-gate-trips"], @@ -229,8 +239,8 @@ def check_airflow(ctx): ctx.check(ok is not None, "the controller did not come back after the check") -@test("commission.check-cameras", title="The cameras check captures both cameras", - subsystem="commission", kind="operator", est_min=2, +@test("setup.check-cameras", title="The cameras check captures both cameras", + subsystem="setup", kind="operator", est_min=2, covers=DARK_COVERS + [("forgectrl", "src/cam.c")], requires=["forgectrl.auth", "camera.snapshot"], actions=["lid"], description="POST /wiz/cameras/start with the lid closed: a lid snapshot, the question, a " @@ -264,8 +274,8 @@ def check_cameras(ctx): shots.get("lid", {}).get("bytes", 0), shots.get("head", {}).get("bytes", 0)) -@test("commission.check-motion", title="The motion check proves the rail, the lens reference, and the jogs", - subsystem="commission", kind="auto", hardware="takeover", est_min=5, +@test("setup.check-motion", title="The motion check proves the rail, the lens reference, and the jogs", + subsystem="setup", kind="auto", hardware="takeover", est_min=5, covers=DARK_COVERS + [("forgectrl", "src/super.c"), ("forgectrl", "src/liveness.c"), ("forgectrl", "src/lenshome.*"), ("forgectrl", "src/accel.c"), ("forgectrl", "src/cool.c")], @@ -305,8 +315,8 @@ def check_motion(ctx): r.get("rest"), {k: (v.get("p2p_lp_x"), v.get("p2p_lp_y")) for k, v in moves.items()}) -@test("commission.check-flow-verify", title="The flow check runs as a setup check", - subsystem="commission", kind="auto", hardware="takeover", est_min=5, +@test("setup.check-flow-verify", title="The flow check runs as a setup check", + subsystem="setup", kind="auto", hardware="takeover", est_min=5, covers=DARK_COVERS + [("forgectrl", "src/diag.c")], requires=["forgectrl.auth", "cooling.aa-offset-calibrate", "cooling.flow-verify"], description="POST /wiz/cooling.flow-verify/start drives the flow-verify diagnostic through the " @@ -323,8 +333,8 @@ def check_flow_verify(ctx): r.get("flow_rise"), r.get("noflow_rise"), r.get("thin_margin")) -@test("commission.cloud-header-capture", title="The cloud header check takes one print's envelope", - subsystem="commission", kind="operator", hardware="takeover", mode="grbl", est_min=8, +@test("setup.cloud-header-capture", title="The cloud header check takes one print's envelope", + subsystem="setup", kind="operator", hardware="takeover", mode="grbl", est_min=8, covers=DARK_COVERS + [("forgectrl", "src/super.c"), ("python3-gfhardware", "gfhardware/machine.py"), ("python3-gfhardware", "forgefirm-app/gfcloud.py"), diff --git a/forgetest/forgetest/suite/commission_sheet.py b/forgetest/forgetest/suite/setup_sheet.py similarity index 96% rename from forgetest/forgetest/suite/commission_sheet.py rename to forgetest/forgetest/suite/setup_sheet.py index 6725e92..0f549d7 100644 --- a/forgetest/forgetest/suite/commission_sheet.py +++ b/forgetest/forgetest/suite/setup_sheet.py @@ -1,4 +1,4 @@ -"""commission.sheet - the setup's sheet (the live wizards) in one run, +"""setup.sheet - the setup's sheet (the live wizards) in one run, driven the way the page drives them: POST /wiz//start, GET /wiz/dark polled, the prompts answered from here, each burn armed by a press, the result judged, and every setting a wizard wrote put back as found. One @@ -18,13 +18,13 @@ restored. The page is where the real numbers go in. import re from ..catalog import test -from .commission_dark import run_check, Restore +from .setup_dark import run_check, Restore SHEET_COVERS = [("forgectrl", "src/wizlive.*"), ("forgectrl", "src/wizrun.h"), ("forgectrl", "src/lens.*"), ("forgectrl", "src/sheet.*"), ("forgectrl", "src/font_hershey.*"), ("forgectrl", "src/jobstream.*"), ("forgectrl", "src/curverec.*"), ("forgectrl", "src/wizdark.*"), - ("forgectrl", "src/wiz.*"), ("forgectrl", "src/commission.*"), + ("forgectrl", "src/wiz.*"), ("forgectrl", "src/setup.*"), ("forgectrl", "src/main.c"), ("forgectrl", "src/ui/wizard.*"), ("forgectrl", "src/super.c"), ("forgectrl", "src/status.c"), ("forgectrl", "src/cool.c"), @@ -222,10 +222,10 @@ def burn(ctx, wid, answers, want): file_card(ctx, wid) -@test("commission.sheet", title="The commissioning sheet: the placement, the frame, and the five cards", - subsystem="commission", kind="live", hardware="takeover", mode="grbl", est_min=35, +@test("setup.sheet", title="The setup sheet: the placement, the frame, and the five cards", + subsystem="setup", kind="live", hardware="takeover", mode="grbl", est_min=35, covers=SHEET_COVERS, - requires=["forgectrl.auth", "commission.check-motion", "laser.emission-witness", "cooling.flow-verify"], + requires=["forgectrl.auth", "setup.check-motion", "laser.emission-witness", "cooling.flow-verify"], actions=["button"], hands=["scrap"], steps=["A piece of wood at least 200 x 150 mm (8 x 6 in) on the bed, pushed as far left as " "it goes with its top edge at the top of the cut area (the head's home corner); " diff --git a/forgetest/tests/test_commission_dark.py b/forgetest/tests/test_setup_dark.py similarity index 62% rename from forgetest/tests/test_commission_dark.py rename to forgetest/tests/test_setup_dark.py index 7452845..13e3053 100644 --- a/forgetest/tests/test_commission_dark.py +++ b/forgetest/tests/test_setup_dark.py @@ -1,8 +1,8 @@ """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.""" +start, the run polled to its end with no question asked, the result +taken, the record read at version 1, and the settings read as before.""" import os import sys import unittest @@ -12,8 +12,8 @@ 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"] +IDS = ["setup.check-switches", "setup.check-sensors", "setup.check-airflow", + "setup.check-cameras", "setup.check-motion", "setup.check-flow-verify"] class RegistrationTests(unittest.TestCase): @@ -25,11 +25,11 @@ class RegistrationTests(unittest.TestCase): # 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"): + for tid in ("setup.check-switches", "setup.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"): + for tid in ("setup.check-sensors", "setup.check-airflow", "setup.check-motion", + "setup.check-flow-verify"): self.assertEqual(self.reg[tid].kind, "auto", tid) def test_no_check_asks_the_operator_to_prepare_anything(self): @@ -37,7 +37,7 @@ class RegistrationTests(unittest.TestCase): 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"): + for tid in ("setup.check-airflow", "setup.check-motion", "setup.check-flow-verify"): self.assertEqual(self.reg[tid].hardware, "takeover", tid) def test_every_check_covers_the_runner_and_the_page(self): @@ -62,9 +62,17 @@ class SensorsCheckTests(unittest.TestCase): "shots": {"lid": False, "head": False}} self.versions = {} self.answers = [] + self.polls = 0 def on_get(path, q): if path == "/wiz/dark": + # One poll sees the sampling; the next sees the result. + if self.dark["running"]: + self.polls += 1 + if self.polls >= 2: + self.dark.update({"running": False, "prompt": None, "progress": 100, + "result": dict(self.RESULT)}) + self.versions["sensors"] = 1 return 200, dict(self.dark) if path == "/wiz": return 200, {"versions": dict(self.versions), "required": [], "completed": True} @@ -74,19 +82,11 @@ class SensorsCheckTests(unittest.TestCase): 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"]}}) + "prompt": None}) 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} + return 409, {"error": "no such prompt is open"} if path == "/settings": for k, v in form.items(): if v == "": @@ -101,15 +101,35 @@ class SensorsCheckTests(unittest.TestCase): def tearDown(self): self.fake.stop() - def test_start_prompt_answer_result_and_restore(self): - t = catalog.load_suite()["commission.check-sensors"] + def test_start_no_question_result_and_settings_untouched(self): + t = catalog.load_suite()["setup.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(self.answers, []) self.assertEqual(run.evidence["result"]["laser_pgood"], 1) - self.assertEqual(run.evidence["settings_before"], {"cool_temp_offset_c": "0.5"}) + self.assertEqual(run.evidence["settings_before"].get("cool_temp_offset_c"), "0.5") self.assertEqual(self.fake.state["settings"]["cool_temp_offset_c"], "0.5") + self.assertNotIn("/settings", [p for p, _ in self.fake.posts]) + + def test_a_question_fails_the_check(self): + t = catalog.load_suite()["setup.check-sensors"] + run = Run("test", t.id, t.title) + real_get = self.fake.on_get + + def on_get(path, q): + if path == "/wiz/dark" and self.dark["running"]: + d = dict(self.dark) + d["prompt"] = {"seq": 3, "id": "room-temp", "kind": "number", "text": "?", + "options": ["Set", "Skip"]} + return 200, d + return real_get(path, q) + self.fake.on_get = on_get + with self.assertRaises(Exception) as cm: + t.fn(Context(run, None, t)) + self.assertIn("asked a question", str(cm.exception)) + self.assertEqual(self.answers, []) + self.assertIn(("/wiz/sensors/abort", {}), [(p, f) for p, f in self.fake.posts]) if __name__ == "__main__": diff --git a/forgetest/tests/test_commission_sheet.py b/forgetest/tests/test_setup_sheet.py similarity index 95% rename from forgetest/tests/test_commission_sheet.py rename to forgetest/tests/test_setup_sheet.py index 1471d05..40906d5 100644 --- a/forgetest/tests/test_commission_sheet.py +++ b/forgetest/tests/test_setup_sheet.py @@ -14,7 +14,7 @@ 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 +from forgetest.suite import setup_sheet # noqa: E402 LIVE = ["sheet.frame", "laser.focus", "laser.floor", "laser.dose-curve", "laser.corner", "cooling.flow-load"] # The focus card's program is read again after it ran: the served ladder @@ -107,7 +107,7 @@ FOUND = {"lens_hall_edge_z_mm": "3.35", "lens_stop_below_steps": "10", "lens_sto class RegistrationTests(unittest.TestCase): def setUp(self): - self.t = catalog.load_suite()["commission.sheet"] + self.t = catalog.load_suite()["setup.sheet"] def test_one_live_test_on_the_button_with_the_sheet_at_hand(self): t = self.t @@ -118,12 +118,12 @@ class RegistrationTests(unittest.TestCase): 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"): + for want in ("setup.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"]) + ids = [i for i in catalog.load_suite() if i.startswith("setup.sheet")] + self.assertEqual(ids, ["setup.sheet"]) def test_covers_the_live_runner_the_renderer_the_sender_the_page_and_the_driver(self): covers = set(self.t.covers) @@ -137,7 +137,7 @@ class RegistrationTests(unittest.TestCase): 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) + self.assertEqual(len(setup_sheet.CARDS), 6) class ScriptedDaemon: @@ -246,7 +246,7 @@ class SheetRunTests(unittest.TestCase): self.fake.stop() def test_the_whole_sheet_on_one_piece(self): - t = catalog.load_suite()["commission.sheet"] + t = catalog.load_suite()["setup.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)) @@ -270,12 +270,12 @@ class SheetRunTests(unittest.TestCase): # frame, the frame's program ran in the fallback window, the focus # card's window was in the settings when the floor card started, # and the window check filed what it compared. - for k in commission_sheet.LENS_SETTINGS: + for k in setup_sheet.LENS_SETTINGS: self.assertNotIn(k, d.settings_at_start["sheet.frame"], k) self.assertEqual(run.evidence["lens_fresh"]["stops_found"], False) self.assertEqual(run.evidence["program_z"]["sheet.frame"]["reach"], [-0.07, 7.46]) floor_start = d.settings_at_start["laser.floor"] - self.assertEqual({k: floor_start[k] for k in commission_sheet.LENS_SETTINGS}, + self.assertEqual({k: floor_start[k] for k in setup_sheet.LENS_SETTINGS}, {"lens_hall_edge_z_mm": "6.96", "lens_stop_below_steps": "14", "lens_stop_above_steps": "20"}) fw = run.evidence["focus_window"] self.assertEqual(fw["window"], FOCUS_WINDOW) @@ -294,7 +294,7 @@ class SheetRunTests(unittest.TestCase): 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"] + t = catalog.load_suite()["setup.sheet"] run = Run("test", t.id, t.title) run.unattended = True t.fn(Context(run, None, t)) @@ -310,7 +310,7 @@ class SheetRunTests(unittest.TestCase): 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"] + t = catalog.load_suite()["setup.sheet"] run = Run("test", t.id, t.title) run.unattended = True with self.assertRaises(Failed) as cm: @@ -326,7 +326,7 @@ class SheetRunTests(unittest.TestCase): 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"] + t = catalog.load_suite()["setup.sheet"] run = Run("test", t.id, t.title) run.unattended = True with self.assertRaises(Failed): @@ -347,7 +347,7 @@ class SheetRunTests(unittest.TestCase): saved = dict(START_WRITES["laser.focus"]) START_WRITES["laser.focus"] = {} try: - t = catalog.load_suite()["commission.sheet"] + t = catalog.load_suite()["setup.sheet"] run = Run("test", t.id, t.title) run.unattended = True with self.assertRaises(Failed) as cm: @@ -364,7 +364,7 @@ class SheetRunTests(unittest.TestCase): # refused before the card starts: nothing burns. from forgetest.runner import Failed self.daemon.stray_z = 10.6 - t = catalog.load_suite()["commission.sheet"] + t = catalog.load_suite()["setup.sheet"] run = Run("test", t.id, t.title) run.unattended = True with self.assertRaises(Failed) as cm: diff --git a/forgetest/tests/test_commission_suite.py b/forgetest/tests/test_setup_suite.py similarity index 73% rename from forgetest/tests/test_commission_suite.py rename to forgetest/tests/test_setup_suite.py index 3c20f54..b3fe890 100644 --- a/forgetest/tests/test_commission_suite.py +++ b/forgetest/tests/test_setup_suite.py @@ -1,4 +1,4 @@ -"""The commission.* suite on the host: the registration (ids, kinds, the +"""The setup.* suite on the host: the registration (ids, kinds, the takeover tests, the operator tests' hands), the record builders, the machine's name, the cookie parsing, the LED cue, the settle rule for a gated supervisor, and the cloud-off surface test driven end to end @@ -12,15 +12,15 @@ import unittest import helpers from forgetest import baseline, catalog from forgetest.runner import Context, Run -from forgetest.suite import commission +from forgetest.suite import setup -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.machine-name", "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") +IDS = ("setup.gate-blocks-controllers", "setup.override-until-reboot", + "setup.advisories-rehash", "setup.account-login", "setup.https-only-writes", + "setup.ssh-until-reboot", "setup.cloud-disabled-surface", + "setup.factory-return", "setup.machine-name", "setup.first-run-flow", + "setup.first-run-page", "setup.what-changed", "setup.record-export", + "setup.mirror") +OPERATOR = ("setup.first-run-flow", "setup.first-run-page") class RegistrationTests(unittest.TestCase): @@ -35,49 +35,49 @@ class RegistrationTests(unittest.TestCase): 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 + self.assertNotIn("setup.root-ssh-refused", self.reg) # folded into ssh-until-reboot + self.assertNotIn("setup.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"): + for tid in ("setup.gate-blocks-controllers", "setup.advisories-rehash", + "setup.first-run-flow", "setup.first-run-page", + "setup.what-changed", "setup.mirror"): self.assertEqual(self.reg[tid].hardware, "takeover", tid) - self.assertEqual(self.reg["commission.record-export"].hardware, "api") + self.assertEqual(self.reg["setup.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) + changed = set(self.reg["setup.what-changed"].covers) + export = set(self.reg["setup.record-export"].covers) + mirror = set(self.reg["setup.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) + self.assertIn("setup.check-sensors", self.reg["setup.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")) + self.assertEqual(setup.flag_of(w, "required", "laser.floor"), "the tube was replaced") + self.assertEqual(setup.flag_of(w, "recommended", "laser.focus"), "the tray was replaced") + self.assertIsNone(setup.flag_of(w, "required", "airflow")) # a plain id is the table, not a flag + self.assertIsNone(setup.flag_of(w, "required", "laser.focus")) + self.assertIsNone(setup.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"] + flow = self.reg["setup.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"] + page = self.reg["setup.first-run-page"] self.assertTrue(page.hands) self.assertFalse(page.fixture_runnable(("button", "lid", "interlock"))) - self.assertIn("commission.first-run-flow", page.requires) + self.assertIn("setup.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.*"), + flow = set(self.reg["setup.first-run-flow"].covers) + page = set(self.reg["setup.first-run-page"].covers) + for want in (("forgectrl", "src/wiz.*"), ("forgectrl", "src/setup.*"), ("forgectrl", "src/users.*"), ("forgectrl", "src/button.*"), ("forgectrl", "src/led.*"), ("forgectrl", "src/advisories.*")): self.assertIn(want, flow, want) self.assertNotIn(want, page, want) @@ -85,17 +85,17 @@ class RegistrationTests(unittest.TestCase): 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 + # With the bench actuator up, three setup 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.") + attended = sorted(tid for tid, t in self.reg.items() if tid.startswith("setup.") 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"]) + self.assertEqual(attended, ["setup.cloud-header-capture", "setup.first-run-page", + "setup.sheet"]) def test_the_factory_return_never_runs_the_return(self): import inspect - t = self.reg["commission.factory-return"] + t = self.reg["setup.factory-return"] src = inspect.getsource(t.fn) self.assertNotIn('"confirm": "1"', src) # the only argument that starts the return self.assertIn('"confirm": "0"', src) @@ -103,15 +103,15 @@ class RegistrationTests(unittest.TestCase): self.assertFalse(t.hands) def test_the_machine_name_covers_nothing_by_design(self): - self.assertEqual(self.reg["commission.machine-name"].covers, ()) + self.assertEqual(self.reg["setup.machine-name"].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"] + t = self.reg["setup.account-login"] self.assertIsNone(getattr(t, "precheck", None)) self.assertEqual(t.hardware, "takeover") - with open(commission.__file__) as f: + with open(setup.__file__) as f: src = f.read() for k in ("FORGETEST_LOGIN_NAME", "FORGETEST_LOGIN_PASSWORD"): self.assertNotIn(k, src) @@ -124,8 +124,8 @@ class RecordTests(unittest.TestCase): "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) + def test_complete_record_counts_as_set_up(self): + rec = setup.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") @@ -139,16 +139,16 @@ class RecordTests(unittest.TestCase): 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) + rec = setup.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 = setup.complete_record(self.STATUS) rec["flags"] = {"machine": {"level": "required", "reason": "x"}} - out = commission.record_without_wizards(rec) + out = setup.record_without_wizards(rec) self.assertEqual(out["wizards"], {}) self.assertEqual(out["flags"], {}) self.assertEqual(out["advisories"], rec["advisories"]) @@ -158,14 +158,14 @@ class RecordTests(unittest.TestCase): 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") + p = os.path.join(tmp, "sub", "setup.json") + setup.write_file(p, b"{}\n") + self.assertEqual(setup.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 + setup.write_file(p, None) + self.assertIsNone(setup.read_file(p)) + setup.write_file(p, None) # a second remove is silent finally: shutil.rmtree(tmp, ignore_errors=True) @@ -173,10 +173,10 @@ class RecordTests(unittest.TestCase): 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") + self.assertEqual(setup.record_path(), "/tmp/ffdata/setup.json") + self.assertEqual(setup.users_path(), "/tmp/ffdata/users") + self.assertEqual(setup.override_path(), "/tmp/ffrun/setup-override") + self.assertEqual(setup.ssh_flag_path(), "/tmp/ffrun/ssh-enabled") finally: os.environ.pop("FORGECTRL_DATA_DIR", None) os.environ.pop("GF_RUN_DIR", None) @@ -188,14 +188,14 @@ class MachineNameTests(unittest.TestCase): self.addCleanup(shutil.rmtree, self.root, ignore_errors=True) def _net(self, **devs): - """A /sys/class/net tree; commission.mac_suffix reads it through - commission.read_file, which takes an absolute path.""" + """A /sys/class/net tree; setup.mac_suffix reads it through + setup.read_file, which takes an absolute path.""" for dev, mac in devs.items(): d = os.path.join(self.root, dev) os.makedirs(d) with open(os.path.join(d, "address"), "w") as f: f.write(mac + "\n") - real = commission.read_file + real = setup.read_file def read(path): head = "/sys/class/net/" @@ -203,52 +203,52 @@ class MachineNameTests(unittest.TestCase): return real(os.path.join(self.root, path[len(head):])) return real(path) - commission.read_file = read - self.addCleanup(setattr, commission, "read_file", real) + setup.read_file = read + self.addCleanup(setattr, setup, "read_file", real) def test_the_wifi_address_names_the_machine(self): self._net(wlan0="2C:6B:7D:0D:B0:0A", eth0="00:11:22:33:44:55") - self.assertEqual(commission.mac_suffix(), "b00a") + self.assertEqual(setup.mac_suffix(), "b00a") def test_a_machine_with_no_wifi_falls_back_to_the_wired_address(self): self._net(eth0="00:11:22:33:44:55") - self.assertEqual(commission.mac_suffix(), "4455") + self.assertEqual(setup.mac_suffix(), "4455") def test_an_unread_address_is_no_address(self): self._net(wlan0="00:00:00:00:00:00") - self.assertEqual(commission.mac_suffix(), "") + self.assertEqual(setup.mac_suffix(), "") def test_no_interface_is_no_address(self): self._net() - self.assertEqual(commission.mac_suffix(), "") + self.assertEqual(setup.mac_suffix(), "") 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("")) + self.assertEqual(setup.session_from_cookie(value), sid) + self.assertEqual(setup.cookie_flags(value), {"path", "httponly", "secure", "samesite", "max-age"}) + self.assertIsNone(setup.session_from_cookie("ffsid=; Path=/; Max-Age=0")) + self.assertIsNone(setup.session_from_cookie("")) def test_the_teal_breathe_cue(self): - self.assertTrue(commission.breathes_teal({"target": [0, 180, 200], "pulse_on": [0, 1400, 1400], + self.assertTrue(setup.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], + self.assertFalse(setup.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], + self.assertFalse(setup.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({})) + self.assertFalse(setup.breathes_teal({"target": [None, None, None], "pulse_on": [None] * 3})) + self.assertFalse(setup.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") + self.assertEqual(setup.decode(b'{"a": 1}'), {"a": 1}) + self.assertEqual(setup.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: + """A supervisor that reports the setup 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.""" @@ -261,7 +261,7 @@ class GatedSettleTests(unittest.TestCase): 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"} + "gated": True, "why": "setup required: machine"} lines = [] body = baseline.Baseline(lines.append).wait_settled(timeout=8) self.assertEqual(body["controller"], "gated") @@ -284,7 +284,7 @@ class FirstRunSeedTests(unittest.TestCase): "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) + rec = setup.first_run_record(self.STATUS, base) self.assertNotIn("completed", rec) self.assertNotIn("acceptance", rec) self.assertNotIn("account", rec) @@ -296,7 +296,7 @@ class FirstRunSeedTests(unittest.TestCase): 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) + rec = setup.first_run_record(self.STATUS, None) self.assertEqual(sorted(rec["wizards"]), ["motion", "sheet.frame", "switches"]) self.assertEqual(rec["wizards"]["sheet.frame"]["version"], 1) @@ -312,7 +312,7 @@ class SshdPolicyTests(unittest.TestCase): real = hw.run hw.run = fake_run try: - self.assertEqual(commission.sshd_policy(), {"permitrootlogin": "no", "passwordauthentication": "yes", + self.assertEqual(setup.sshd_policy(), {"permitrootlogin": "no", "passwordauthentication": "yes", "permitemptypasswords": "no"}) finally: hw.run = real @@ -323,7 +323,7 @@ class SshdPolicyTests(unittest.TestCase): 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"}) + self.assertEqual(setup.sshd_policy(), {"error": "sshd: no hostkeys available"}) finally: hw.run = real @@ -375,7 +375,7 @@ class CloudDisabledSurfaceTests(unittest.TestCase): self.fake.stop() def run_test(self): - t = catalog.load_suite()["commission.cloud-disabled-surface"] + t = catalog.load_suite()["setup.cloud-disabled-surface"] run = Run("test", t.id, t.title) t.fn(Context(run, None, t)) return run