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.
This commit is contained in:
ScottW514
2026-09-14 10:37:55 -04:00
parent c0d4302d73
commit d8b8adaef4
12 changed files with 243 additions and 213 deletions
+2 -2
View File
@@ -22,13 +22,13 @@ case "$1" in
start) start)
echo "Starting forgetest" echo "Starting forgetest"
mkdir -p "$DATA" 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 # the controller gate until the next reboot (/run is tmpfs), so a
# campaign runs on a machine whose required wizards are not complete. # campaign runs on a machine whose required wizards are not complete.
# Nothing lifts the agreements or the account: the bench accepts them # Nothing lifts the agreements or the account: the bench accepts them
# once, and that stays in /data. # once, and that stays in /data.
mkdir -p /run/forgefirm 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 \ start-stop-daemon -S -q -p $PIDFILE -m -b -x /bin/sh -- -c \
"exec /usr/bin/python3 -m forgetest >> $LOG 2>&1" "exec /usr/bin/python3 -m forgetest >> $LOG 2>&1"
;; ;;
+1 -1
View File
@@ -321,7 +321,7 @@ class Baseline:
def wait_settled(self, timeout=SETTLE_S, unreachable_s=10): def wait_settled(self, timeout=SETTLE_S, unreachable_s=10):
"""Block until forgectrl reports a settled supervisor: motion """Block until forgectrl reports a settled supervisor: motion
verified (the probe passed), motion-fault (the ladder exhausted), 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 is closed: no controller spawns until it opens). Gives up after
unreachable_s without an answer. Returns the last /mode body (None unreachable_s without an answer. Returns the last /mode body (None
if unreachable).""" if unreachable)."""
+3 -3
View File
@@ -4,9 +4,9 @@ order. Each module registers its tests with @catalog.test."""
from . import image # noqa: F401,E402 from . import image # noqa: F401,E402
from . import kernel # noqa: F401,E402 from . import kernel # noqa: F401,E402
from . import forgectrl # noqa: F401,E402 from . import forgectrl # noqa: F401,E402
from . import commission # noqa: F401,E402 from . import setup # noqa: F401,E402
from . import commission_dark # noqa: F401,E402 from . import setup_dark # noqa: F401,E402
from . import commission_sheet # noqa: F401,E402 from . import setup_sheet # noqa: F401,E402
from . import logs # noqa: F401,E402 from . import logs # noqa: F401,E402
from . import motion # noqa: F401,E402 from . import motion # noqa: F401,E402
from . import cooling # noqa: F401,E402 from . import cooling # noqa: F401,E402
+1 -1
View File
@@ -396,7 +396,7 @@ def lid_privacy(ctx):
"authorizes a write; a rotation from the panel route stops the old key and " "authorizes a write; a rotation from the panel route stops the old key and "
"the new one reads. The setting is put back after.") "the new one reads. The setting is put back after.")
def key_read(ctx): def key_read(ctx):
from .commission import request from .setup import request
from .forgectrl import lan_ip from .forgectrl import lan_ip
fc = ctx.forgectrl fc = ctx.forgectrl
ev = ctx.evidence ev = ctx.evidence
+1 -1
View File
@@ -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 " "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 " "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 " "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): def crash_watch_plumbing(ctx):
fc = ctx.forgectrl fc = ctx.forgectrl
ev = ctx.evidence ev = ctx.evidence
+1 -1
View File
@@ -152,7 +152,7 @@ def auth(ctx):
# HTTPS the route itself refuses the peer (403 loopback only). Neither # HTTPS the route itself refuses the peer (403 loopback only). Neither
# request follows the redirect, and the self-signed certificate is # request follows the redirect, and the self-signed certificate is
# not verified. # not verified.
from .commission import request, decode from .setup import request, decode
ip = lan_ip() ip = lan_ip()
ev["lan_ip"] = ip ev["lan_ip"] = ip
ctx.check(ip, "cannot determine the board's LAN address") ctx.check(ip, "cannot determine the board's LAN address")
@@ -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 controller gate, the advisories, the account and its login, the HTTPS
boundary, SSH, the cloud switch, the machine's name, and the factory boundary, SSH, the cloud switch, the machine's name, and the factory
return. 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 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 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 a different record installs the file under a forgectrl restart (a
@@ -34,13 +34,13 @@ from ..catalog import test
from .. import hw from .. import hw
from .forgectrl import lan_ip from .forgectrl import lan_ip
RECORD = "commissioning.json" RECORD = "setup.json"
USERS = "users" USERS = "users"
OVERRIDE = "commissioning-override" OVERRIDE = "setup-override"
SSH_FLAG = "ssh-enabled" SSH_FLAG = "ssh-enabled"
TLS_BASE = "https://127.0.0.1" # FORGECTRL_TLS_URL overrides (host tests) 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") REQUIRED_WIZARDS = ("advisories", "account", "preferences", "machine", "cloud")
ADVISORY_DOCS = ("safety-and-risk", "licenses", "privacy", "cloud-service") ADVISORY_DOCS = ("safety-and-risk", "licenses", "privacy", "cloud-service")
SAFETY_PHRASE = "I UNDERSTAND" SAFETY_PHRASE = "I UNDERSTAND"
@@ -176,8 +176,8 @@ def record_without_wizards(record):
def complete_record(status, base=None, account_name="bench"): def complete_record(status, base=None, account_name="bench"):
"""A complete commissioning record for a machine that must count as """A complete setup record for a machine that must count as
commissioned: every advisory at the hash GET /wiz reports, the set up: every advisory at the hash GET /wiz reports, the
press recorded, an account (the one of `base` when it has one), press recorded, an account (the one of `base` when it has one),
every wizard of the catalog at its version, and the run complete. 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 `status` is a GET /wiz body; `base` is an existing record to keep
@@ -352,12 +352,12 @@ def gate_is_open():
# -------------------------------------------------------------- the gate # -------------------------------------------------------------- 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")] ("forgectrl", "src/main.c"), ("forgectrl", "src/paths.h")]
@test("commission.gate-blocks-controllers", title="The commissioning gate spawns no controller", @test("setup.gate-blocks-controllers", title="The setup gate spawns no controller",
subsystem="commission", kind="auto", hardware="takeover", mode="grbl", est_min=5, subsystem="setup", kind="auto", hardware="takeover", mode="grbl", est_min=5,
covers=_GATE_COVERS, requires=["forgectrl.auth"], precheck=gate_is_open, covers=_GATE_COVERS, requires=["forgectrl.auth"], precheck=gate_is_open,
description="With the override removed and a record that lacks every required wizard " description="With the override removed and a record that lacks every required wizard "
"(installed under a forgectrl restart, the consent and the account kept), " "(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"), ctx.log("before: gate %s, override %s, required %s", before.get("gate"), before.get("override"),
before.get("required")) before.get("required"))
raw = read_file(record_path()) 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: try:
record = json.loads(raw.decode("utf-8")) record = json.loads(raw.decode("utf-8"))
except ValueError as e: 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" lacking = json.dumps(record_without_wizards(record), indent=1, sort_keys=True).encode() + b"\n"
had_override = override_present() had_override = override_present()
ev["override_before"] = had_override 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.log("/mode after the restart: %s (after %s s)", m, took)
ctx.check(m.get("controller") == "gated" and m.get("gated") is True, ctx.check(m.get("controller") == "gated" and m.get("gated") is True,
"the supervisor did not report the gate: %s", m) "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")) "the reason does not name the missing wizards: %r", m.get("why"))
w = wiz(fc) w = wiz(fc)
ev["gated_wiz"] = wiz_summary(w) 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")) 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", @test("setup.override-until-reboot", title="The override stands until the next reboot",
subsystem="commission", kind="auto", est_min=1, subsystem="setup", kind="auto", est_min=1,
covers=_GATE_COVERS, requires=["forgectrl.auth"], covers=_GATE_COVERS, requires=["forgectrl.auth"],
description="The bench seed: the dev image's forgetest init script writes " 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 " "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 " "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 " "gated. The file lives on tmpfs (/proc/mounts), so a reboot removes it by "
@@ -486,9 +486,9 @@ def override_until_reboot(ctx):
# ------------------------------------------------------------ advisories # ------------------------------------------------------------ advisories
@test("commission.advisories-rehash", title="An advisory is accepted at its current hash only", @test("setup.advisories-rehash", title="An advisory is accepted at its current hash only",
subsystem="commission", kind="auto", hardware="takeover", est_min=3, subsystem="setup", kind="auto", hardware="takeover", est_min=3,
covers=[("forgectrl", "src/advisories.*"), ("forgectrl", "src/commission.*"), covers=[("forgectrl", "src/advisories.*"), ("forgectrl", "src/setup.*"),
("forgectrl", "src/wiz.*"), ("forgectrl", "src/sha256.*"), ("forgectrl", "src/main.c"), ("forgectrl", "src/wiz.*"), ("forgectrl", "src/sha256.*"), ("forgectrl", "src/main.c"),
("forgectrl", "src/ui/md.js"), ("forgectrl", "src/ui/embed_docs.cmake")], ("forgectrl", "src/ui/md.js"), ("forgectrl", "src/ui/embed_docs.cmake")],
requires=["forgectrl.auth"], requires=["forgectrl.auth"],
@@ -504,7 +504,7 @@ def advisories_rehash(ctx):
fc = ctx.forgectrl fc = ctx.forgectrl
ev = ctx.evidence ev = ctx.evidence
raw = read_file(record_path()) 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) before = wiz(fc)
ev["before"] = wiz_summary(before) ev["before"] = wiz_summary(before)
docs = {d.get("id"): d for d in before.get("documents") or []} docs = {d.get("id"): d for d in before.get("documents") or []}
@@ -579,7 +579,7 @@ def advisories_rehash(ctx):
# -------------------------------------------------------------- the login # -------------------------------------------------------------- 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, kind="auto", est_min=2,
covers=[("forgectrl", "src/auth.*"), ("forgectrl", "src/session.*"), ("forgectrl", "src/users.*"), covers=[("forgectrl", "src/auth.*"), ("forgectrl", "src/session.*"), ("forgectrl", "src/users.*"),
("forgectrl", "src/tls.*"), ("forgectrl", "src/peer.*"), ("forgectrl", "src/main.c"), ("forgectrl", "src/tls.*"), ("forgectrl", "src/peer.*"), ("forgectrl", "src/main.c"),
@@ -702,7 +702,7 @@ def login_checks(ctx, name, pw):
# ------------------------------------------------------- HTTPS boundary # ------------------------------------------------------- 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, kind="auto", est_min=1,
covers=[("forgectrl", "src/auth.*"), ("forgectrl", "src/tls.*"), ("forgectrl", "src/peer.*"), covers=[("forgectrl", "src/auth.*"), ("forgectrl", "src/tls.*"), ("forgectrl", "src/peer.*"),
("forgectrl", "src/main.c")], ("forgectrl", "src/main.c")],
@@ -802,8 +802,8 @@ def restore_ssh(fc, before):
write_file(flag, b"" if before.get("enabled") else None) 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", @test("setup.ssh-until-reboot", title="SSH is switched from the panel until the next reboot",
subsystem="commission", kind="auto", est_min=1, subsystem="setup", kind="auto", est_min=1,
covers=[("forgectrl", "src/main.c"), ("forgectrl", "src/paths.h")], covers=[("forgectrl", "src/main.c"), ("forgectrl", "src/paths.h")],
requires=["forgectrl.auth"], requires=["forgectrl.auth"],
description="GET /system/ssh reports enabled, running, and dev_image, and agrees with the " description="GET /system/ssh reports enabled, running, and dev_image, and agrees with the "
@@ -888,8 +888,8 @@ def ssh_until_reboot(ctx):
# ---------------------------------------------------------------- cloud # ---------------------------------------------------------------- cloud
@test("commission.cloud-disabled-surface", title="Nothing points at the cloud while it is off", @test("setup.cloud-disabled-surface", title="Nothing points at the cloud while it is off",
subsystem="commission", kind="auto", mode="grbl", est_min=1, subsystem="setup", kind="auto", mode="grbl", est_min=1,
covers=[("forgectrl", "src/main.c"), ("forgectrl", "src/super.c"), ("forgectrl", "src/settings.*"), covers=[("forgectrl", "src/main.c"), ("forgectrl", "src/super.c"), ("forgectrl", "src/settings.*"),
("forgectrl", "src/wiz.c"), ("forgectrl", "src/hooks.h")], ("forgectrl", "src/wiz.c"), ("forgectrl", "src/hooks.h")],
requires=["forgectrl.settings-bounds"], requires=["forgectrl.settings-bounds"],
@@ -971,8 +971,8 @@ def cloud_disabled_surface(ctx):
# ---------------------------------------------------------- the operator # ---------------------------------------------------------- the operator
@test("commission.factory-return", title="The return to the factory firmware is guarded", @test("setup.factory-return", title="The return to the factory firmware is guarded",
subsystem="commission", kind="auto", est_min=1, subsystem="setup", kind="auto", est_min=1,
covers=[("forgectrl", "src/update.*"), ("forgectrl", "src/main.c"), ("forgectrl", "src/ui/wizard.js")], covers=[("forgectrl", "src/update.*"), ("forgectrl", "src/main.c"), ("forgectrl", "src/ui/wizard.js")],
requires=["update.slots-and-signature"], requires=["update.slots-and-signature"],
description="The return itself reboots the machine as a Glowforge, so it never runs from the " description="The return itself reboots the machine as a Glowforge, so it never runs from the "
@@ -1034,8 +1034,8 @@ def cmdlines():
return out return out
@test("commission.machine-name", title="The machine names itself from its MAC address", @test("setup.machine-name", title="The machine names itself from its MAC address",
subsystem="commission", kind="auto", est_min=1, subsystem="setup", kind="auto", est_min=1,
requires=["forgectrl.auth"], requires=["forgectrl.auth"],
description="The machine calls itself forgefirm-<xxxx>, where xxxx is the last four hex " description="The machine calls itself forgefirm-<xxxx>, where xxxx is the last four hex "
"digits of its WiFi MAC address, so two machines on one network answer to " "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) "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, 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/session.*"), ("forgectrl", "src/button.*"), ("forgectrl", "src/led.*"),
("forgectrl", "src/sheetid.*"), ("forgectrl", "src/advisories.*"), ("forgectrl", "src/hooks.h"), ("forgectrl", "src/sheetid.*"), ("forgectrl", "src/advisories.*"), ("forgectrl", "src/hooks.h"),
("forgectrl", "src/main.c")], ("forgectrl", "src/main.c")],
@@ -1309,11 +1309,11 @@ def first_run_flow(ctx):
ctx.log("the run completed: gate open, the controller back") 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, kind="operator", hardware="takeover", est_min=6,
covers=[("forgectrl", "src/ui/wizard.*"), ("forgectrl", "src/ui/forms.js"), ("forgectrl", "src/ui/md.js"), 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.*")], ("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"], actions=["button"], hands=["workstation"],
steps=["From the workstation open https://<address>/ and accept the self-signed certificate.", steps=["From the workstation open https://<address>/ and accept the self-signed certificate.",
"Click through the setup: scroll each document to its end and accept it (type I " "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 " "clicks through (the documents accepted, the press step with the LEDs "
"breathing teal, the account created, the run complete, the gate open, the " "breathing teal, the account created, the run complete, the gate open, the "
"controller back). The operator confirms the panel opened. Everything the run " "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): def first_run_page(ctx):
fc = ctx.forgectrl fc = ctx.forgectrl
ev = ctx.evidence ev = ctx.evidence
@@ -1364,8 +1364,8 @@ def first_run_page(ctx):
ctx.confirm("Did the control panel open after Finish?") ctx.confirm("Did the control panel open after Finish?")
@test("commission.cert-page", title="The certificate is checkable before the warning", @test("setup.cert-page", title="The certificate is checkable before the warning",
subsystem="commission", kind="auto", est_min=1, subsystem="setup", kind="auto", est_min=1,
covers=[("forgectrl", "src/tls.*"), ("forgectrl", "src/main.c"), covers=[("forgectrl", "src/tls.*"), ("forgectrl", "src/main.c"),
("forgectrl", "src/ui/wizard.html"), ("forgectrl", "src/ui/wizard.js")], ("forgectrl", "src/ui/wizard.html"), ("forgectrl", "src/ui/wizard.js")],
requires=["forgectrl.auth"], requires=["forgectrl.auth"],
@@ -1423,9 +1423,9 @@ def flag_of(w, level, wid):
return None return None
@test("commission.what-changed", title="A replaced part asks for its checks again", @test("setup.what-changed", title="A replaced part asks for its checks again",
subsystem="commission", kind="auto", est_min=2, hardware="takeover", subsystem="setup", kind="auto", est_min=2, hardware="takeover",
covers=[("forgectrl", "src/commission.*"), ("forgectrl", "src/wiz.*"), ("forgectrl", "src/main.c"), covers=[("forgectrl", "src/setup.*"), ("forgectrl", "src/wiz.*"), ("forgectrl", "src/main.c"),
("forgectrl", "src/ui/panel.js"), ("forgectrl", "src/ui/index.html"), ("forgectrl", "src/ui/panel.js"), ("forgectrl", "src/ui/index.html"),
("forgectrl", "src/ui/help.js")], ("forgectrl", "src/ui/help.js")],
requires=["forgectrl.auth"], 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") 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", @test("setup.record-export", title="The record: JSON, the printable page, and the log bundle",
subsystem="commission", kind="auto", est_min=2, subsystem="setup", kind="auto", est_min=2,
covers=[("forgectrl", "src/commission.*"), ("forgectrl", "src/recordhtml.*"), ("forgectrl", "src/wiz.*"), covers=[("forgectrl", "src/setup.*"), ("forgectrl", "src/recordhtml.*"), ("forgectrl", "src/wiz.*"),
("forgectrl", "src/logs.*"), ("forgectrl", "src/main.c"), ("forgectrl", "src/ui/index.html"), ("forgectrl", "src/logs.*"), ("forgectrl", "src/main.c"), ("forgectrl", "src/ui/index.html"),
("forgectrl", "src/ui/panel.js"), ("forgectrl", "src/ui/wizard.html")], ("forgectrl", "src/ui/panel.js"), ("forgectrl", "src/ui/wizard.html")],
requires=["forgectrl.auth", "logs.tree-tail-export"], 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 " "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 " "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 " "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): def record_export(ctx):
import gzip import gzip
import io 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", ctx.check(st == 200 and "text/html" in (hdrs.get("content-type") or ""), "GET /wiz/record.html -> %s %s",
st, hdrs.get("content-type")) st, hdrs.get("content-type"))
ctx.check("<script" not in page.lower(), "the printable page carries a script") ctx.check("<script" not in page.lower(), "the printable page carries a script")
ctx.check(sid in page and "commissioning record" in page, "the page lacks the sheet id or its title") ctx.check(sid in page and "setup record" in page, "the page lacks the sheet id or its title")
titles = {z["id"]: z["title"] for z in w.get("wizards") or []} titles = {z["id"]: z["title"] for z in w.get("wizards") or []}
missing = [d for d in done if titles.get(d) and ("<h3>" + titles[d]) not in page] missing = [d for d in done if titles.get(d) and ("<h3>" + titles[d]) not in page]
ev["missing_on_page"] = missing ev["missing_on_page"] = missing
@@ -1565,11 +1565,11 @@ def record_export(ctx):
try: try:
tf = tarfile.open(fileobj=io.BytesIO(gzip.decompress(data))) tf = tarfile.open(fileobj=io.BytesIO(gzip.decompress(data)))
for m in tf.getmembers(): 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() member = tf.extractfile(m).read()
except (OSError, tarfile.TarError, EOFError) as e: except (OSError, tarfile.TarError, EOFError) as e:
ctx.fail("export is not a readable tar.gz: %s", 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: if member is not None:
try: try:
inside = json.loads(member.decode("utf-8")) 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"]) 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", @test("setup.mirror", title="One browser drives a check; a second one follows",
subsystem="commission", kind="auto", est_min=2, hardware="takeover", subsystem="setup", kind="auto", est_min=2, hardware="takeover",
covers=[("forgectrl", "src/wizdark.*"), ("forgectrl", "src/wizcalc.*"), ("forgectrl", "src/wiz.*"), covers=[("forgectrl", "src/wizdark.*"), ("forgectrl", "src/wizcalc.*"), ("forgectrl", "src/wiz.*"),
("forgectrl", "src/session.*"), ("forgectrl", "src/ui/wizard.js"), ("forgectrl", "src/session.*"), ("forgectrl", "src/ui/wizard.js"),
("forgectrl", "src/ui/wizard.html"), ("forgectrl", "src/ui/wizard.css")], ("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 " 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 " "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, " "reports the run as owned and its own to the first session, not to the second, "
@@ -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/<id>/start, GET /wiz/dark polled, way the page drives them: POST /wiz/<id>/start, GET /wiz/dark polled,
the prompts answered from here (the bench fixture works the lid and the the prompts answered from here (the bench fixture works the lid and the
button; a confirmation is answered yes once the snapshot exists), the button; a confirmation is answered yes once the snapshot exists), the
@@ -12,7 +12,7 @@ import time
from ..catalog import test from ..catalog import test
from .. import hw from .. import hw
from ..baseline import read_position 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 POLL_S = 1.0
# What the machine's own press prompt says, used when it sends no text. # 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. # (74) for the first seconds and settles near 630.
PURGE_SPINUP_S = 30 PURGE_SPINUP_S = 30
DARK_COVERS = [("forgectrl", "src/wizdark.*"), ("forgectrl", "src/wizcalc.*"), 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.*")] ("forgectrl", "src/main.c"), ("forgectrl", "src/ui/wizard.*")]
@@ -113,8 +113,8 @@ def run_check(ctx, wid, on_prompt, timeout_s):
return last return last
@test("commission.check-switches", title="The switches check follows the lid and the button", @test("setup.check-switches", title="The switches check follows the lid and the button",
subsystem="commission", kind="operator", est_min=3, subsystem="setup", kind="operator", est_min=3,
covers=DARK_COVERS + [("forgectrl", "src/status.c")], covers=DARK_COVERS + [("forgectrl", "src/status.c")],
requires=["forgectrl.auth"], actions=["lid", "button"], requires=["forgectrl.auth"], actions=["lid", "button"],
description="POST /wiz/switches/start; the check asks for the lid to open and close and for " 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)) ctx.log("switches: %s", json.dumps(r))
@test("commission.check-sensors", title="The sensors check reads a plausible machine at rest", @test("setup.check-sensors", title="The sensors check reads a plausible machine at rest",
subsystem="commission", kind="auto", est_min=2, subsystem="setup", kind="auto", est_min=2,
covers=DARK_COVERS + [("forgectrl", "src/status.c"), ("forgectrl", "src/accel.c"), covers=DARK_COVERS + [("forgectrl", "src/status.c"), ("forgectrl", "src/accel.c"),
("forgectrl", "src/cool.c")], ("forgectrl", "src/cool.c")],
requires=["forgectrl.auth"], requires=["forgectrl.auth"],
description="POST /wiz/sensors/start; ten seconds of readings, then the room-temperature " description="POST /wiz/sensors/start; ten seconds of readings and no question asked. The "
"prompt, answered Skip so no offset is written. The result carries both coolant " "result carries both coolant temperatures, the chassis and SoC, the lid IR "
"temperatures, the chassis and SoC, the lid IR maxima, the accelerometer event " "maxima, the accelerometer event count, the supply power-good, the HV current, "
"count, the supply power-good, the HV current, and the idle fan speeds; the " "and the idle fan speeds; the record carries sensors at version 1 and the "
"record carries sensors at version 1 and cool_temp_offset_c reads as before.") "settings read as before (the check writes none).")
def check_sensors(ctx): def check_sensors(ctx):
with Restore(ctx, ["cool_temp_offset_c"]): fc = ctx.forgectrl
last = run_check(ctx, "sensors", lambda p: "Skip" if p.get("id") == "room-temp" else None, 120) 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 {} r = last.get("result") or {}
for k in ("coolant_down_c", "coolant_up_c", "lid_ir_max", "laser_pgood", "hv_current_max", for k in ("coolant_down_c", "coolant_up_c", "lid_ir_max", "laser_pgood", "hv_current_max",
"exhaust_rpm_idle", "intake_rpm_idle"): "exhaust_rpm_idle", "intake_rpm_idle"):
ctx.check(k in r, "the result lacks %s: %s", k, r) 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.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"), 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")) 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", @test("setup.check-airflow", title="The airflow check measures the fans and sets the floors",
subsystem="commission", kind="auto", hardware="takeover", est_min=3, subsystem="setup", kind="auto", hardware="takeover", est_min=3,
covers=DARK_COVERS + [("forgectrl", "src/cool.c"), ("forgectrl", "src/airflow.*"), covers=DARK_COVERS + [("forgectrl", "src/cool.c"), ("forgectrl", "src/airflow.*"),
("forgectrl", "src/gates.c"), ("forgectrl", "src/super.c")], ("forgectrl", "src/gates.c"), ("forgectrl", "src/super.c")],
requires=["forgectrl.auth", "cooling.fan-gate-trips"], 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") 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", @test("setup.check-cameras", title="The cameras check captures both cameras",
subsystem="commission", kind="operator", est_min=2, subsystem="setup", kind="operator", est_min=2,
covers=DARK_COVERS + [("forgectrl", "src/cam.c")], covers=DARK_COVERS + [("forgectrl", "src/cam.c")],
requires=["forgectrl.auth", "camera.snapshot"], actions=["lid"], requires=["forgectrl.auth", "camera.snapshot"], actions=["lid"],
description="POST /wiz/cameras/start with the lid closed: a lid snapshot, the question, a " 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)) 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", @test("setup.check-motion", title="The motion check proves the rail, the lens reference, and the jogs",
subsystem="commission", kind="auto", hardware="takeover", est_min=5, subsystem="setup", kind="auto", hardware="takeover", est_min=5,
covers=DARK_COVERS + [("forgectrl", "src/super.c"), ("forgectrl", "src/liveness.c"), covers=DARK_COVERS + [("forgectrl", "src/super.c"), ("forgectrl", "src/liveness.c"),
("forgectrl", "src/lenshome.*"), ("forgectrl", "src/lenshome.*"),
("forgectrl", "src/accel.c"), ("forgectrl", "src/cool.c")], ("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()}) 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", @test("setup.check-flow-verify", title="The flow check runs as a setup check",
subsystem="commission", kind="auto", hardware="takeover", est_min=5, subsystem="setup", kind="auto", hardware="takeover", est_min=5,
covers=DARK_COVERS + [("forgectrl", "src/diag.c")], covers=DARK_COVERS + [("forgectrl", "src/diag.c")],
requires=["forgectrl.auth", "cooling.aa-offset-calibrate", "cooling.flow-verify"], requires=["forgectrl.auth", "cooling.aa-offset-calibrate", "cooling.flow-verify"],
description="POST /wiz/cooling.flow-verify/start drives the flow-verify diagnostic through the " 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")) 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", @test("setup.cloud-header-capture", title="The cloud header check takes one print's envelope",
subsystem="commission", kind="operator", hardware="takeover", mode="grbl", est_min=8, subsystem="setup", kind="operator", hardware="takeover", mode="grbl", est_min=8,
covers=DARK_COVERS + [("forgectrl", "src/super.c"), covers=DARK_COVERS + [("forgectrl", "src/super.c"),
("python3-gfhardware", "gfhardware/machine.py"), ("python3-gfhardware", "gfhardware/machine.py"),
("python3-gfhardware", "forgefirm-app/gfcloud.py"), ("python3-gfhardware", "forgefirm-app/gfcloud.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/<id>/start, GET /wiz/dark driven the way the page drives them: POST /wiz/<id>/start, GET /wiz/dark
polled, the prompts answered from here, each burn armed by a press, the 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 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 import re
from ..catalog import test 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"), SHEET_COVERS = [("forgectrl", "src/wizlive.*"), ("forgectrl", "src/wizrun.h"),
("forgectrl", "src/lens.*"), ("forgectrl", "src/sheet.*"), ("forgectrl", "src/lens.*"), ("forgectrl", "src/sheet.*"),
("forgectrl", "src/font_hershey.*"), ("forgectrl", "src/jobstream.*"), ("forgectrl", "src/font_hershey.*"), ("forgectrl", "src/jobstream.*"),
("forgectrl", "src/curverec.*"), ("forgectrl", "src/wizdark.*"), ("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/main.c"), ("forgectrl", "src/ui/wizard.*"),
("forgectrl", "src/super.c"), ("forgectrl", "src/super.c"),
("forgectrl", "src/status.c"), ("forgectrl", "src/cool.c"), ("forgectrl", "src/status.c"), ("forgectrl", "src/cool.c"),
@@ -222,10 +222,10 @@ def burn(ctx, wid, answers, want):
file_card(ctx, wid) file_card(ctx, wid)
@test("commission.sheet", title="The commissioning sheet: the placement, the frame, and the five cards", @test("setup.sheet", title="The setup sheet: the placement, the frame, and the five cards",
subsystem="commission", kind="live", hardware="takeover", mode="grbl", est_min=35, subsystem="setup", kind="live", hardware="takeover", mode="grbl", est_min=35,
covers=SHEET_COVERS, 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"], 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 " 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); " "it goes with its top edge at the top of the cut area (the head's home corner); "
@@ -1,8 +1,8 @@
"""The setup's checks in the catalog: registered as automatic tests with """The setup's checks in the catalog: registered as automatic tests with
no precheck, the takeover ones marked so, every covers map naming the no precheck, the takeover ones marked so, every covers map naming the
check runner; and the check driver itself against the fake daemon: a check runner; and the check driver itself against the fake daemon: a
start, a prompt answered, the result taken, the record read at version start, the run polled to its end with no question asked, the result
1, and the settings a check may write put back as found.""" taken, the record read at version 1, and the settings read as before."""
import os import os
import sys import sys
import unittest import unittest
@@ -12,8 +12,8 @@ import helpers # noqa: E402
from forgetest import catalog # noqa: E402 from forgetest import catalog # noqa: E402
from forgetest.runner import Context, Run # noqa: E402 from forgetest.runner import Context, Run # noqa: E402
IDS = ["commission.check-switches", "commission.check-sensors", "commission.check-airflow", IDS = ["setup.check-switches", "setup.check-sensors", "setup.check-airflow",
"commission.check-cameras", "commission.check-motion", "commission.check-flow-verify"] "setup.check-cameras", "setup.check-motion", "setup.check-flow-verify"]
class RegistrationTests(unittest.TestCase): class RegistrationTests(unittest.TestCase):
@@ -25,11 +25,11 @@ class RegistrationTests(unittest.TestCase):
# bench fixture performs; the rest need no hands at all. # bench fixture performs; the rest need no hands at all.
for tid in IDS: for tid in IDS:
self.assertIn(tid, self.reg) 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.assertEqual(self.reg[tid].kind, "operator", tid)
self.assertTrue(self.reg[tid].fixture_runnable(("button", "lid", "interlock")), tid) self.assertTrue(self.reg[tid].fixture_runnable(("button", "lid", "interlock")), tid)
for tid in ("commission.check-sensors", "commission.check-airflow", "commission.check-motion", for tid in ("setup.check-sensors", "setup.check-airflow", "setup.check-motion",
"commission.check-flow-verify"): "setup.check-flow-verify"):
self.assertEqual(self.reg[tid].kind, "auto", tid) self.assertEqual(self.reg[tid].kind, "auto", tid)
def test_no_check_asks_the_operator_to_prepare_anything(self): 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) self.assertIsNone(getattr(self.reg[tid], "precheck", None), tid)
def test_the_checks_that_take_the_machine_say_so(self): 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) self.assertEqual(self.reg[tid].hardware, "takeover", tid)
def test_every_check_covers_the_runner_and_the_page(self): def test_every_check_covers_the_runner_and_the_page(self):
@@ -62,9 +62,17 @@ class SensorsCheckTests(unittest.TestCase):
"shots": {"lid": False, "head": False}} "shots": {"lid": False, "head": False}}
self.versions = {} self.versions = {}
self.answers = [] self.answers = []
self.polls = 0
def on_get(path, q): def on_get(path, q):
if path == "/wiz/dark": 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) return 200, dict(self.dark)
if path == "/wiz": if path == "/wiz":
return 200, {"versions": dict(self.versions), "required": [], "completed": True} return 200, {"versions": dict(self.versions), "required": [], "completed": True}
@@ -74,19 +82,11 @@ class SensorsCheckTests(unittest.TestCase):
s = self.fake.state["settings"] s = self.fake.state["settings"]
if path == "/wiz/sensors/start": if path == "/wiz/sensors/start":
self.dark.update({"id": "sensors", "running": True, "phase": "sampling", self.dark.update({"id": "sensors", "running": True, "phase": "sampling",
"prompt": {"seq": 7, "id": "room-temp", "kind": "number", "prompt": None})
"text": "Optional: the room temperature.",
"options": ["Set", "Skip"]}})
return 200, {"started": True, "id": "sensors"} return 200, {"started": True, "id": "sensors"}
if path == "/wiz/sensors/answer": if path == "/wiz/sensors/answer":
self.answers.append(dict(form)) self.answers.append(dict(form))
if form.get("seq") != "7":
return 409, {"error": "no such prompt is open"} 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": if path == "/settings":
for k, v in form.items(): for k, v in form.items():
if v == "": if v == "":
@@ -101,15 +101,35 @@ class SensorsCheckTests(unittest.TestCase):
def tearDown(self): def tearDown(self):
self.fake.stop() self.fake.stop()
def test_start_prompt_answer_result_and_restore(self): def test_start_no_question_result_and_settings_untouched(self):
t = catalog.load_suite()["commission.check-sensors"] t = catalog.load_suite()["setup.check-sensors"]
run = Run("test", t.id, t.title) run = Run("test", t.id, t.title)
t.fn(Context(run, None, t)) t.fn(Context(run, None, t))
self.assertIn(("/wiz/sensors/start", {}), [(p, f) for p, f in self.fake.posts]) 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["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.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__": if __name__ == "__main__":
@@ -14,7 +14,7 @@ sys.path.insert(0, os.path.dirname(__file__))
import helpers # noqa: E402 import helpers # noqa: E402
from forgetest import catalog # noqa: E402 from forgetest import catalog # noqa: E402
from forgetest.runner import Context, Run # 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"] 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 # 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): class RegistrationTests(unittest.TestCase):
def setUp(self): 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): def test_one_live_test_on_the_button_with_the_sheet_at_hand(self):
t = self.t t = self.t
@@ -118,12 +118,12 @@ class RegistrationTests(unittest.TestCase):
self.assertTrue(t.hands) self.assertTrue(t.hands)
self.assertFalse(t.fixture_runnable(("button", "lid", "interlock"))) self.assertFalse(t.fixture_runnable(("button", "lid", "interlock")))
self.assertIsNone(getattr(t, "precheck", None)) 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) self.assertIn(want, t.requires)
def test_no_other_sheet_test_remains(self): def test_no_other_sheet_test_remains(self):
ids = [i for i in catalog.load_suite() if i.startswith("commission.sheet")] ids = [i for i in catalog.load_suite() if i.startswith("setup.sheet")]
self.assertEqual(ids, ["commission.sheet"]) self.assertEqual(ids, ["setup.sheet"])
def test_covers_the_live_runner_the_renderer_the_sender_the_page_and_the_driver(self): def test_covers_the_live_runner_the_renderer_the_sender_the_page_and_the_driver(self):
covers = set(self.t.covers) covers = set(self.t.covers)
@@ -137,7 +137,7 @@ class RegistrationTests(unittest.TestCase):
text = " ".join(self.t.steps).lower() text = " ".join(self.t.steps).lower()
self.assertIn("press the machine's button once", text) self.assertIn("press the machine's button once", text)
self.assertNotIn("when the page", text) self.assertNotIn("when the page", text)
self.assertEqual(len(commission_sheet.CARDS), 6) self.assertEqual(len(setup_sheet.CARDS), 6)
class ScriptedDaemon: class ScriptedDaemon:
@@ -246,7 +246,7 @@ class SheetRunTests(unittest.TestCase):
self.fake.stop() self.fake.stop()
def test_the_whole_sheet_on_one_piece(self): 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 = Run("test", t.id, t.title)
run.unattended = True # the ready gate passes; the arm cue is a notice run.unattended = True # the ready gate passes; the arm cue is a notice
t.fn(Context(run, None, t)) 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 # frame, the frame's program ran in the fallback window, the focus
# card's window was in the settings when the floor card started, # card's window was in the settings when the floor card started,
# and the window check filed what it compared. # 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.assertNotIn(k, d.settings_at_start["sheet.frame"], k)
self.assertEqual(run.evidence["lens_fresh"]["stops_found"], False) self.assertEqual(run.evidence["lens_fresh"]["stops_found"], False)
self.assertEqual(run.evidence["program_z"]["sheet.frame"]["reach"], [-0.07, 7.46]) self.assertEqual(run.evidence["program_z"]["sheet.frame"]["reach"], [-0.07, 7.46])
floor_start = d.settings_at_start["laser.floor"] 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"}) {"lens_hall_edge_z_mm": "6.96", "lens_stop_below_steps": "14", "lens_stop_above_steps": "20"})
fw = run.evidence["focus_window"] fw = run.evidence["focus_window"]
self.assertEqual(fw["window"], FOCUS_WINDOW) self.assertEqual(fw["window"], FOCUS_WINDOW)
@@ -294,7 +294,7 @@ class SheetRunTests(unittest.TestCase):
place_prompts, place_result = SCRIPT["sheet.place"] place_prompts, place_result = SCRIPT["sheet.place"]
SCRIPT["sheet.place"] = (place_prompts, dict(place_result, thickness_mm=3.175)) SCRIPT["sheet.place"] = (place_prompts, dict(place_result, thickness_mm=3.175))
try: try:
t = catalog.load_suite()["commission.sheet"] t = catalog.load_suite()["setup.sheet"]
run = Run("test", t.id, t.title) run = Run("test", t.id, t.title)
run.unattended = True run.unattended = True
t.fn(Context(run, None, t)) t.fn(Context(run, None, t))
@@ -310,7 +310,7 @@ class SheetRunTests(unittest.TestCase):
place_prompts, place_result = SCRIPT["sheet.place"] place_prompts, place_result = SCRIPT["sheet.place"]
SCRIPT["sheet.place"] = (place_prompts, dict(place_result, thickness_mm=0.0)) SCRIPT["sheet.place"] = (place_prompts, dict(place_result, thickness_mm=0.0))
try: try:
t = catalog.load_suite()["commission.sheet"] t = catalog.load_suite()["setup.sheet"]
run = Run("test", t.id, t.title) run = Run("test", t.id, t.title)
run.unattended = True run.unattended = True
with self.assertRaises(Failed) as cm: 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_DARK["emission"] = {"hv_max": 0, "laser_on_samples": 0, "thermopile_delta": 0}
SCRIPT["laser.floor"] = (SCRIPT["laser.floor"][0], SCRIPT_DARK) SCRIPT["laser.floor"] = (SCRIPT["laser.floor"][0], SCRIPT_DARK)
try: try:
t = catalog.load_suite()["commission.sheet"] t = catalog.load_suite()["setup.sheet"]
run = Run("test", t.id, t.title) run = Run("test", t.id, t.title)
run.unattended = True run.unattended = True
with self.assertRaises(Failed): with self.assertRaises(Failed):
@@ -347,7 +347,7 @@ class SheetRunTests(unittest.TestCase):
saved = dict(START_WRITES["laser.focus"]) saved = dict(START_WRITES["laser.focus"])
START_WRITES["laser.focus"] = {} START_WRITES["laser.focus"] = {}
try: try:
t = catalog.load_suite()["commission.sheet"] t = catalog.load_suite()["setup.sheet"]
run = Run("test", t.id, t.title) run = Run("test", t.id, t.title)
run.unattended = True run.unattended = True
with self.assertRaises(Failed) as cm: with self.assertRaises(Failed) as cm:
@@ -364,7 +364,7 @@ class SheetRunTests(unittest.TestCase):
# refused before the card starts: nothing burns. # refused before the card starts: nothing burns.
from forgetest.runner import Failed from forgetest.runner import Failed
self.daemon.stray_z = 10.6 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 = Run("test", t.id, t.title)
run.unattended = True run.unattended = True
with self.assertRaises(Failed) as cm: with self.assertRaises(Failed) as cm:
@@ -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 takeover tests, the operator tests' hands), the record builders, the
machine's name, the cookie parsing, the LED cue, the settle rule for a 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 gated supervisor, and the cloud-off surface test driven end to end
@@ -12,15 +12,15 @@ import unittest
import helpers import helpers
from forgetest import baseline, catalog from forgetest import baseline, catalog
from forgetest.runner import Context, Run 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", IDS = ("setup.gate-blocks-controllers", "setup.override-until-reboot",
"commission.advisories-rehash", "commission.account-login", "commission.https-only-writes", "setup.advisories-rehash", "setup.account-login", "setup.https-only-writes",
"commission.ssh-until-reboot", "commission.cloud-disabled-surface", "setup.ssh-until-reboot", "setup.cloud-disabled-surface",
"commission.factory-return", "commission.machine-name", "commission.first-run-flow", "setup.factory-return", "setup.machine-name", "setup.first-run-flow",
"commission.first-run-page", "commission.what-changed", "commission.record-export", "setup.first-run-page", "setup.what-changed", "setup.record-export",
"commission.mirror") "setup.mirror")
OPERATOR = ("commission.first-run-flow", "commission.first-run-page") OPERATOR = ("setup.first-run-flow", "setup.first-run-page")
class RegistrationTests(unittest.TestCase): class RegistrationTests(unittest.TestCase):
@@ -35,49 +35,49 @@ class RegistrationTests(unittest.TestCase):
for tid in IDS: for tid in IDS:
if tid not in OPERATOR: if tid not in OPERATOR:
self.assertEqual(kinds[tid], "auto", tid) self.assertEqual(kinds[tid], "auto", tid)
self.assertNotIn("commission.root-ssh-refused", self.reg) # folded into ssh-until-reboot self.assertNotIn("setup.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.wizard-first-run", self.reg) # split into the flow and the page
def test_the_record_swapping_tests_are_takeovers(self): def test_the_record_swapping_tests_are_takeovers(self):
for tid in ("commission.gate-blocks-controllers", "commission.advisories-rehash", for tid in ("setup.gate-blocks-controllers", "setup.advisories-rehash",
"commission.first-run-flow", "commission.first-run-page", "setup.first-run-flow", "setup.first-run-page",
"commission.what-changed", "commission.mirror"): "setup.what-changed", "setup.mirror"):
self.assertEqual(self.reg[tid].hardware, "takeover", tid) 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): def test_the_lifecycle_tests_cover_their_files(self):
changed = set(self.reg["commission.what-changed"].covers) changed = set(self.reg["setup.what-changed"].covers)
export = set(self.reg["commission.record-export"].covers) export = set(self.reg["setup.record-export"].covers)
mirror = set(self.reg["commission.mirror"].covers) mirror = set(self.reg["setup.mirror"].covers)
self.assertIn(("forgectrl", "src/ui/panel.js"), changed) self.assertIn(("forgectrl", "src/ui/panel.js"), changed)
self.assertIn(("forgectrl", "src/recordhtml.*"), export) self.assertIn(("forgectrl", "src/recordhtml.*"), export)
self.assertIn(("forgectrl", "src/logs.*"), export) self.assertIn(("forgectrl", "src/logs.*"), export)
self.assertIn(("forgectrl", "src/wizdark.*"), mirror) 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): def test_flag_of_reads_the_status_document(self):
w = {"required": ["airflow", {"id": "laser.floor", "reason": "the tube was replaced"}], w = {"required": ["airflow", {"id": "laser.floor", "reason": "the tube was replaced"}],
"recommended": [{"id": "laser.focus", "reason": "the tray 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(setup.flag_of(w, "required", "laser.floor"), "the tube was replaced")
self.assertEqual(commission.flag_of(w, "recommended", "laser.focus"), "the tray was replaced") self.assertEqual(setup.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(setup.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(setup.flag_of(w, "required", "laser.focus"))
self.assertIsNone(commission.flag_of({}, "recommended", "x")) 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): 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.assertEqual(list(flow.actions), ["button"])
self.assertFalse(flow.hands) self.assertFalse(flow.hands)
self.assertTrue(flow.fixture_runnable(("button", "lid", "interlock"))) 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.assertTrue(page.hands)
self.assertFalse(page.fixture_runnable(("button", "lid", "interlock"))) 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): def test_the_first_run_split_keeps_the_backend_and_the_page_apart(self):
flow = set(self.reg["commission.first-run-flow"].covers) flow = set(self.reg["setup.first-run-flow"].covers)
page = set(self.reg["commission.first-run-page"].covers) page = set(self.reg["setup.first-run-page"].covers)
for want in (("forgectrl", "src/wiz.*"), ("forgectrl", "src/commission.*"), ("forgectrl", "src/users.*"), for want in (("forgectrl", "src/wiz.*"), ("forgectrl", "src/setup.*"), ("forgectrl", "src/users.*"),
("forgectrl", "src/button.*"), ("forgectrl", "src/led.*"), ("forgectrl", "src/advisories.*")): ("forgectrl", "src/button.*"), ("forgectrl", "src/led.*"), ("forgectrl", "src/advisories.*")):
self.assertIn(want, flow, want) self.assertIn(want, flow, want)
self.assertNotIn(want, page, want) self.assertNotIn(want, page, want)
@@ -85,17 +85,17 @@ class RegistrationTests(unittest.TestCase):
self.assertIn(want, page, want) self.assertIn(want, page, want)
def test_the_only_attended_tests_need_a_workstation_or_the_sheet(self): 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 # person: the page walk, the print from the Glowforge app, and
# the sheet. Everything else runs from the queue. # 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"))) and t.kind != "auto" and not t.fixture_runnable(("button", "lid", "interlock")))
self.assertEqual(attended, ["commission.cloud-header-capture", "commission.first-run-page", self.assertEqual(attended, ["setup.cloud-header-capture", "setup.first-run-page",
"commission.sheet"]) "setup.sheet"])
def test_the_factory_return_never_runs_the_return(self): def test_the_factory_return_never_runs_the_return(self):
import inspect import inspect
t = self.reg["commission.factory-return"] t = self.reg["setup.factory-return"]
src = inspect.getsource(t.fn) src = inspect.getsource(t.fn)
self.assertNotIn('"confirm": "1"', src) # the only argument that starts the return self.assertNotIn('"confirm": "1"', src) # the only argument that starts the return
self.assertIn('"confirm": "0"', src) self.assertIn('"confirm": "0"', src)
@@ -103,15 +103,15 @@ class RegistrationTests(unittest.TestCase):
self.assertFalse(t.hands) self.assertFalse(t.hands)
def test_the_machine_name_covers_nothing_by_design(self): 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): def test_the_login_test_makes_its_own_account(self):
# No bench credentials, no precheck: the test installs a temporary # No bench credentials, no precheck: the test installs a temporary
# account under a takeover and restores the real one. # 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.assertIsNone(getattr(t, "precheck", None))
self.assertEqual(t.hardware, "takeover") self.assertEqual(t.hardware, "takeover")
with open(commission.__file__) as f: with open(setup.__file__) as f:
src = f.read() src = f.read()
for k in ("FORGETEST_LOGIN_NAME", "FORGETEST_LOGIN_PASSWORD"): for k in ("FORGETEST_LOGIN_NAME", "FORGETEST_LOGIN_PASSWORD"):
self.assertNotIn(k, src) self.assertNotIn(k, src)
@@ -124,8 +124,8 @@ class RecordTests(unittest.TestCase):
"wizards": [{"id": "advisories", "version": 1}, {"id": "account", "version": 1}, "wizards": [{"id": "advisories", "version": 1}, {"id": "account", "version": 1},
{"id": "machine", "version": 2}]} {"id": "machine", "version": 2}]}
def test_complete_record_counts_as_commissioned(self): def test_complete_record_counts_as_set_up(self):
rec = commission.complete_record(self.STATUS) rec = setup.complete_record(self.STATUS)
self.assertEqual(rec["schema"], 1) self.assertEqual(rec["schema"], 1)
self.assertEqual(rec["advisories"]["safety-and-risk"]["hash"], "a" * 64) self.assertEqual(rec["advisories"]["safety-and-risk"]["hash"], "a" * 64)
self.assertEqual(rec["advisories"]["safety-and-risk"]["method"], "typed") 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): def test_complete_record_keeps_the_base_account_and_machine(self):
base = {"account": {"name": "owner", "uid": 1000, "created": "x"}, "machine": {"model": "pro"}, base = {"account": {"name": "owner", "uid": 1000, "created": "x"}, "machine": {"model": "pro"},
"sheet_id": "KKKKK-LLLLL", "created": "then"} "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["account"]["name"], "owner")
self.assertEqual(rec["machine"], {"model": "pro"}) self.assertEqual(rec["machine"], {"model": "pro"})
self.assertEqual(rec["sheet_id"], "KKKKK-LLLLL") self.assertEqual(rec["sheet_id"], "KKKKK-LLLLL")
self.assertEqual(rec["created"], "then") self.assertEqual(rec["created"], "then")
def test_without_wizards_keeps_consent_and_account(self): 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"}} 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["wizards"], {})
self.assertEqual(out["flags"], {}) self.assertEqual(out["flags"], {})
self.assertEqual(out["advisories"], rec["advisories"]) self.assertEqual(out["advisories"], rec["advisories"])
@@ -158,14 +158,14 @@ class RecordTests(unittest.TestCase):
def test_write_file_and_remove(self): def test_write_file_and_remove(self):
tmp = tempfile.mkdtemp(prefix="forgetest-comm-") tmp = tempfile.mkdtemp(prefix="forgetest-comm-")
try: try:
p = os.path.join(tmp, "sub", "commissioning.json") p = os.path.join(tmp, "sub", "setup.json")
commission.write_file(p, b"{}\n") setup.write_file(p, b"{}\n")
self.assertEqual(commission.read_file(p), b"{}\n") self.assertEqual(setup.read_file(p), b"{}\n")
if os.name == "posix": # a mode means nothing on a Windows host if os.name == "posix": # a mode means nothing on a Windows host
self.assertEqual(oct(os.stat(p).st_mode & 0o777), oct(0o600)) self.assertEqual(oct(os.stat(p).st_mode & 0o777), oct(0o600))
commission.write_file(p, None) setup.write_file(p, None)
self.assertIsNone(commission.read_file(p)) self.assertIsNone(setup.read_file(p))
commission.write_file(p, None) # a second remove is silent setup.write_file(p, None) # a second remove is silent
finally: finally:
shutil.rmtree(tmp, ignore_errors=True) shutil.rmtree(tmp, ignore_errors=True)
@@ -173,10 +173,10 @@ class RecordTests(unittest.TestCase):
os.environ["FORGECTRL_DATA_DIR"] = "/tmp/ffdata" os.environ["FORGECTRL_DATA_DIR"] = "/tmp/ffdata"
os.environ["GF_RUN_DIR"] = "/tmp/ffrun" os.environ["GF_RUN_DIR"] = "/tmp/ffrun"
try: try:
self.assertEqual(commission.record_path(), "/tmp/ffdata/commissioning.json") self.assertEqual(setup.record_path(), "/tmp/ffdata/setup.json")
self.assertEqual(commission.users_path(), "/tmp/ffdata/users") self.assertEqual(setup.users_path(), "/tmp/ffdata/users")
self.assertEqual(commission.override_path(), "/tmp/ffrun/commissioning-override") self.assertEqual(setup.override_path(), "/tmp/ffrun/setup-override")
self.assertEqual(commission.ssh_flag_path(), "/tmp/ffrun/ssh-enabled") self.assertEqual(setup.ssh_flag_path(), "/tmp/ffrun/ssh-enabled")
finally: finally:
os.environ.pop("FORGECTRL_DATA_DIR", None) os.environ.pop("FORGECTRL_DATA_DIR", None)
os.environ.pop("GF_RUN_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) self.addCleanup(shutil.rmtree, self.root, ignore_errors=True)
def _net(self, **devs): def _net(self, **devs):
"""A /sys/class/net tree; commission.mac_suffix reads it through """A /sys/class/net tree; setup.mac_suffix reads it through
commission.read_file, which takes an absolute path.""" setup.read_file, which takes an absolute path."""
for dev, mac in devs.items(): for dev, mac in devs.items():
d = os.path.join(self.root, dev) d = os.path.join(self.root, dev)
os.makedirs(d) os.makedirs(d)
with open(os.path.join(d, "address"), "w") as f: with open(os.path.join(d, "address"), "w") as f:
f.write(mac + "\n") f.write(mac + "\n")
real = commission.read_file real = setup.read_file
def read(path): def read(path):
head = "/sys/class/net/" head = "/sys/class/net/"
@@ -203,52 +203,52 @@ class MachineNameTests(unittest.TestCase):
return real(os.path.join(self.root, path[len(head):])) return real(os.path.join(self.root, path[len(head):]))
return real(path) return real(path)
commission.read_file = read setup.read_file = read
self.addCleanup(setattr, commission, "read_file", real) self.addCleanup(setattr, setup, "read_file", real)
def test_the_wifi_address_names_the_machine(self): 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._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): def test_a_machine_with_no_wifi_falls_back_to_the_wired_address(self):
self._net(eth0="00:11:22:33:44:55") 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): def test_an_unread_address_is_no_address(self):
self._net(wlan0="00:00:00:00:00:00") 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): def test_no_interface_is_no_address(self):
self._net() self._net()
self.assertEqual(commission.mac_suffix(), "") self.assertEqual(setup.mac_suffix(), "")
class SmallHelpersTests(unittest.TestCase): class SmallHelpersTests(unittest.TestCase):
def test_cookie_parsing(self): def test_cookie_parsing(self):
sid = "ab" * 32 sid = "ab" * 32
value = "ffsid=%s; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=43200" % sid value = "ffsid=%s; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=43200" % sid
self.assertEqual(commission.session_from_cookie(value), sid) self.assertEqual(setup.session_from_cookie(value), sid)
self.assertEqual(commission.cookie_flags(value), {"path", "httponly", "secure", "samesite", "max-age"}) self.assertEqual(setup.cookie_flags(value), {"path", "httponly", "secure", "samesite", "max-age"})
self.assertIsNone(commission.session_from_cookie("ffsid=; Path=/; Max-Age=0")) self.assertIsNone(setup.session_from_cookie("ffsid=; Path=/; Max-Age=0"))
self.assertIsNone(commission.session_from_cookie("")) self.assertIsNone(setup.session_from_cookie(""))
def test_the_teal_breathe_cue(self): 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]})) "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 "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 "pulse_off": [1800] * 3})) # white
self.assertFalse(commission.breathes_teal({"target": [None, None, None], "pulse_on": [None] * 3})) self.assertFalse(setup.breathes_teal({"target": [None, None, None], "pulse_on": [None] * 3}))
self.assertFalse(commission.breathes_teal({})) self.assertFalse(setup.breathes_teal({}))
def test_decode(self): def test_decode(self):
self.assertEqual(commission.decode(b'{"a": 1}'), {"a": 1}) self.assertEqual(setup.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"cloud mode is not enabled"), "cloud mode is not enabled")
class GatedSettleTests(unittest.TestCase): 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 it spawns nothing until the gate opens, so a takeover that installs
a gating record must not wait the whole settle timeout for it.""" 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): def test_gated_returns_at_once(self):
self.fake.state["mode"] = {"mode": "grbl", "controller": "gated", "pid": 0, "motion": "unverified", 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 = [] lines = []
body = baseline.Baseline(lines.append).wait_settled(timeout=8) body = baseline.Baseline(lines.append).wait_settled(timeout=8)
self.assertEqual(body["controller"], "gated") self.assertEqual(body["controller"], "gated")
@@ -284,7 +284,7 @@ class FirstRunSeedTests(unittest.TestCase):
"advisories": {"safety-and-risk": {"hash": "x"}}, "acceptance": {"pressed_at": "t"}, "advisories": {"safety-and-risk": {"hash": "x"}}, "acceptance": {"pressed_at": "t"},
"account": {"name": "scott", "uid": 1000}, "flags": {"flow_thin": True}, "account": {"name": "scott", "uid": 1000}, "flags": {"flow_thin": True},
"wizards": {"advisories": {"version": 1}, "switches": {"version": 1, "result": {"lid": 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("completed", rec)
self.assertNotIn("acceptance", rec) self.assertNotIn("acceptance", rec)
self.assertNotIn("account", 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 self.assertEqual(rec["wizards"]["motion"]["version"], 2) # written at the catalog version
def test_without_a_base_record(self): 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(sorted(rec["wizards"]), ["motion", "sheet.frame", "switches"])
self.assertEqual(rec["wizards"]["sheet.frame"]["version"], 1) self.assertEqual(rec["wizards"]["sheet.frame"]["version"], 1)
@@ -312,7 +312,7 @@ class SshdPolicyTests(unittest.TestCase):
real = hw.run real = hw.run
hw.run = fake_run hw.run = fake_run
try: try:
self.assertEqual(commission.sshd_policy(), {"permitrootlogin": "no", "passwordauthentication": "yes", self.assertEqual(setup.sshd_policy(), {"permitrootlogin": "no", "passwordauthentication": "yes",
"permitemptypasswords": "no"}) "permitemptypasswords": "no"})
finally: finally:
hw.run = real hw.run = real
@@ -323,7 +323,7 @@ class SshdPolicyTests(unittest.TestCase):
real = hw.run real = hw.run
hw.run = lambda cmd, timeout=60: (1, "sshd: no hostkeys available") hw.run = lambda cmd, timeout=60: (1, "sshd: no hostkeys available")
try: try:
self.assertEqual(commission.sshd_policy(), {"error": "sshd: no hostkeys available"}) self.assertEqual(setup.sshd_policy(), {"error": "sshd: no hostkeys available"})
finally: finally:
hw.run = real hw.run = real
@@ -375,7 +375,7 @@ class CloudDisabledSurfaceTests(unittest.TestCase):
self.fake.stop() self.fake.stop()
def run_test(self): 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) run = Run("test", t.id, t.title)
t.fn(Context(run, None, t)) t.fn(Context(run, None, t))
return run return run