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)
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"
;;
+1 -1
View File
@@ -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)."""
+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 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
+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 "
"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
+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 "
"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
+1 -1
View File
@@ -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")
@@ -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-<xxxx>, 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://<address>/ 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("<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 []}
missing = [d for d in done if titles.get(d) and ("<h3>" + 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, "
@@ -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,
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"),
@@ -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
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); "
@@ -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__":
@@ -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:
@@ -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