mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-10-15 17:19:55 -07:00
forgetest: the setup required up to the intake fans; the burns replace the sheet
The catalog follows forgectrl's setup (openglow-org/forgefirm#4): the gate is the advisories with the press, the account, the required steps (preferences, machine, cloud), and the intake fan count; every other step is recommended; the setup sheet is retired for two self-contained burns and a dark lens travel check; the override file is gone. - setup.gate-blocks-controllers installs four records: without consent, without the account, and without the three required steps the controller is gated with the reason naming the cause; with them done the gate opens, the controller runs, and the other steps are recommended as not run. Clearing cool_intake_fans closes the gate live with its reason, and putting it back as found reopens it. - setup.override-until-reboot is deleted with its helpers, and forgetest.init no longer writes the override file at boot. - setup.first-run-flow follows the reason from the three required steps to open; setup.first-run-page walks the choice screen after them; setup.what-changed reads the one-level map with the gate unmoved. - New setup.check-intakes (the count by the tachs equals the machine's, the setting put back as found) and setup.check-lens (the window written equals the stops found or the fallback, and the settings and /status hold it; both keys restored). A precheck refuses the intake and airflow checks while cool_intake_fans is unset, so neither can leave it unset in the middle of a campaign. - setup_burns.py replaces setup_sheet.py: setup.focus and setup.floor answer the gantry stops, the home, the scrap and the thickness, and keep the preview, emission and window checks; motion.tray starts the focus burn for its tray-out refusal. Proof: the forgetest host suite (529 before the required-steps edits, and the full suite after them) and the manifest load pass; pyflakes reports no undefined names. The coverage lint is clean against forgectrl's working tree; against the current pin, setup.check-lens's src/lensfind.* glob selects no file until the forgectrl pin moves to the commit that adds it. Acceptance: these are the tests themselves; the campaign runs them with cool_intake_fans set on the bench.
This commit is contained in:
@@ -0,0 +1,414 @@
|
||||
# Copyright 2026 514 LLC d/b/a OpenGlow
|
||||
# Written by Scott Wiederhold
|
||||
# https://community.openglow.org
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""The setup's two burns in the catalog: two live tests on the button with
|
||||
a scrap at hand, their covers naming the live runner, the renderer, the
|
||||
sender, the page, and the driver's laser module; and each burn against a
|
||||
scripted daemon: the preview and the program read, the start, the
|
||||
prompts answered in order (the gantry stops, the head at home, the scrap,
|
||||
its thickness in the machine's units, the arm, the machine's own press
|
||||
prompt, the pick), the witnesses judged, the record read at version 1,
|
||||
and the setting the burn wrote put back as found."""
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
import helpers # noqa: E402
|
||||
from forgetest import catalog # noqa: E402
|
||||
from forgetest.runner import Context, Failed, Run # noqa: E402
|
||||
from forgetest.suite import setup_burns, setup_dark # noqa: E402
|
||||
|
||||
LIVE = ["laser.focus", "laser.floor"]
|
||||
EMISSION = {"hv_max": 610, "laser_on_samples": 900, "thermopile_delta": 600, "lit_s": 30.0}
|
||||
SPM = 2.922 # the lens screw, half-steps per mm ($102)
|
||||
EDGE_DEFAULT = 3.35 # the edge's height while the setting is unset (lens.h)
|
||||
|
||||
|
||||
def lens_block(s):
|
||||
"""The /status lens block as the daemon builds it from the settings: a
|
||||
stop count from 1 to 40 stands, anything else is the fallback."""
|
||||
def count(key, fallback):
|
||||
try:
|
||||
n = float(s.get(key, ""))
|
||||
except ValueError:
|
||||
return fallback, False
|
||||
return (int(n), True) if 1 <= n <= 40 else (fallback, False)
|
||||
try:
|
||||
edge = float(s.get("lens_hall_edge_z_mm", ""))
|
||||
except ValueError:
|
||||
edge = EDGE_DEFAULT
|
||||
below, a = count("lens_stop_below_steps", 10)
|
||||
above, b = count("lens_stop_above_steps", 12)
|
||||
return {"edge_z": round(edge, 2), "below": below, "above": above, "stops_found": a and b,
|
||||
"reach_min": round(edge - below / SPM, 2), "reach_max": round(edge + above / SPM, 2)}
|
||||
|
||||
|
||||
def ladder_z(edge, below, above):
|
||||
"""The focus ladder the daemon builds: twelve whole half-steps from
|
||||
the top of the window to the bottom."""
|
||||
return [round(edge + round(above - (above + below) * i / 11) / SPM, 2) for i in range(12)]
|
||||
|
||||
|
||||
# Each burn as the daemon runs it: its prompts in order (id, kind,
|
||||
# options) and the result it ends with. The press prompt is the
|
||||
# machine's own: nothing answers it, the discharge closes it. The focus
|
||||
# burn's window and ladder are the ones the settings held at its start.
|
||||
SETUP = [("gantry-stops", "confirm", ["Yes", "No"]),
|
||||
("home", "choice", ["The head is home", "Release the motors"]),
|
||||
("scrap", "continue", ["Continue"]), ("thickness", "number", ["Set"])]
|
||||
PRESS = ("press", "wait", [])
|
||||
SCRIPT = {
|
||||
"laser.focus": (SETUP + [("focus-arm", "continue", ["Continue"]), PRESS,
|
||||
("focus-pick", "multichoice", [str(i) for i in range(1, 13)])],
|
||||
{"z_passes": [100, 101], "emission": EMISSION, "pick": 11, "picked_lines": [11, 11],
|
||||
"pick_half_steps": -8, "edge_z_mm": 6.96, "steps_per_mm": SPM, "max_height_mm": 13.81,
|
||||
"focus_range_mm": {"min": 2.17, "max": 13.81}, "summary": "Line 11."}),
|
||||
"laser.floor": (SETUP + [("floor-arm", "continue", ["Continue"]), PRESS,
|
||||
("floor-pick", "choice", [str(2 * i) for i in range(1, 13)] + ["None"])],
|
||||
{"z_passes": [100, 101], "emission": EMISSION, "faintest_density": 8.0,
|
||||
"floor_density": 10.0, "summary": "The floor is 10."}),
|
||||
}
|
||||
# What each burn writes when it ends.
|
||||
WRITES = {"laser.focus": {"lens_hall_edge_z_mm": "6.96"}, "laser.floor": {"laser_floor_density": "10"}}
|
||||
# The machine as the burns find it: its own edge, the window the Lens
|
||||
# travel check wrote, its floor.
|
||||
FOUND = {"lens_hall_edge_z_mm": "4.10", "lens_stop_below_steps": "14", "lens_stop_above_steps": "20",
|
||||
"laser_floor_density": "12", "ui_units": "metric"}
|
||||
HOME = "The head is home"
|
||||
|
||||
|
||||
class RegistrationTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.reg = catalog.load_suite()
|
||||
|
||||
def test_two_live_tests_on_the_button_with_a_scrap_at_hand(self):
|
||||
for tid in ("setup.focus", "setup.floor"):
|
||||
t = self.reg[tid]
|
||||
self.assertEqual(t.kind, "live", tid)
|
||||
self.assertEqual(t.mode, "grbl", tid)
|
||||
self.assertEqual(t.hardware, "takeover", tid)
|
||||
self.assertEqual(list(t.actions), ["button"], tid)
|
||||
self.assertEqual(list(t.hands), ["scrap"], tid)
|
||||
self.assertFalse(t.fixture_runnable(("button", "lid", "interlock")), tid)
|
||||
self.assertIsNone(getattr(t, "precheck", None), tid)
|
||||
self.assertEqual(list(self.reg["setup.focus"].requires),
|
||||
["forgectrl.auth", "setup.check-motion", "laser.emission-witness", "setup.check-lens"])
|
||||
self.assertEqual(list(self.reg["setup.floor"].requires),
|
||||
["forgectrl.auth", "setup.check-motion", "laser.emission-witness"])
|
||||
|
||||
def test_the_sheet_is_gone(self):
|
||||
self.assertFalse([i for i in self.reg if i.startswith("setup.sheet")])
|
||||
|
||||
def test_covers_the_live_runner_the_renderer_the_sender_the_page_and_the_driver(self):
|
||||
for tid in ("setup.focus", "setup.floor"):
|
||||
covers = set(self.reg[tid].covers)
|
||||
for want in (("forgectrl", "src/wizlive.*"), ("forgectrl", "src/sheet.*"),
|
||||
("forgectrl", "src/jobstream.*"), ("forgectrl", "src/curverec.*"),
|
||||
("forgectrl", "src/font_hershey.*"), ("forgectrl", "src/lens.*"),
|
||||
("forgectrl", "src/ui/wizard.*"), ("forgectrl", "src/wizrun.h"),
|
||||
("grblhal-glowforge", "src/glowforge_laser.c")):
|
||||
self.assertIn(want, covers, "%s lacks %s" % (tid, want))
|
||||
|
||||
def test_the_steps_ask_for_a_scrap_in_the_corner_and_one_press(self):
|
||||
for tid, size in (("setup.focus", "78 x 68 mm"), ("setup.floor", "86 x 68 mm")):
|
||||
text = " ".join(self.reg[tid].steps)
|
||||
self.assertIn(size, text, tid)
|
||||
self.assertIn("back-left corner", text, tid)
|
||||
self.assertIn("gantry stops", text, tid)
|
||||
self.assertIn("press the machine's button once", text, tid)
|
||||
|
||||
|
||||
class ScriptedDaemon:
|
||||
"""The wizard runner as the suite sees it: /wiz/<id>/start opens the
|
||||
first prompt, each answer opens the next (a thickness out of range in
|
||||
the machine's units opens it again), the press prompt closes on the
|
||||
poll after the one that served it, the last answer ends the burn with
|
||||
its result and its writes, /wiz/dark reports it, GET /wiz carries the
|
||||
versions, and the preview and the program are served per burn."""
|
||||
|
||||
def __init__(self, fake):
|
||||
self.fake = fake
|
||||
fake.state["settings"].update(FOUND)
|
||||
self.dark = {"id": "", "running": False, "phase": "", "progress": 0, "elapsed_s": 0,
|
||||
"log": [], "prompt": None, "result": None, "error": "",
|
||||
"shots": {"lid": False, "head": False}}
|
||||
self.versions = {}
|
||||
self.answers = {} # wid -> [values]
|
||||
self.order = [] # the burns as started
|
||||
self.aborted = []
|
||||
self.settings_at_start = {}
|
||||
self.previews = []
|
||||
self.queue = []
|
||||
self.seq = 0
|
||||
self.press_seen = False
|
||||
self.thickness = None
|
||||
self.lens_at_start = None
|
||||
self.units = None # the units the number prompts read; None = ui_units
|
||||
self.start_writes = {} # wid -> settings the burn writes as it starts
|
||||
self.patch = {} # wid -> result fields that differ from the script
|
||||
self.stray_z = None # a Z the served program commands beyond the reach, when set
|
||||
fake.on_get = self.on_get
|
||||
fake.on_post = self.on_post
|
||||
|
||||
def open_next(self):
|
||||
if not self.queue:
|
||||
return False
|
||||
pid, kind, options = self.queue[0]
|
||||
self.seq += 1
|
||||
self.dark["prompt"] = {"seq": self.seq, "id": pid, "kind": kind, "text": pid, "options": options}
|
||||
return True
|
||||
|
||||
def finish(self, wid):
|
||||
s = self.fake.state["settings"]
|
||||
r = dict(SCRIPT[wid][1], thickness_mm=round(self.thickness, 3))
|
||||
if wid == "laser.focus":
|
||||
lens = self.lens_at_start
|
||||
r["window"] = {k: lens[k] for k in ("below", "above", "reach_min", "reach_max")}
|
||||
r["ladder_z"] = ladder_z(lens["edge_z"], lens["below"], lens["above"])
|
||||
r.update(self.patch.get(wid, {}))
|
||||
s.update(WRITES[wid])
|
||||
self.dark.update({"running": False, "prompt": None, "progress": 100, "result": r})
|
||||
self.versions[wid] = 1
|
||||
|
||||
def on_get(self, path, q):
|
||||
s = self.fake.state["settings"]
|
||||
if path == "/status":
|
||||
# the lens block follows the settings, as the daemon's does
|
||||
return 200, dict(self.fake.state["status"], lens=lens_block(s))
|
||||
if path == "/wiz/dark":
|
||||
p = self.dark.get("prompt")
|
||||
if p and p["id"] == "press":
|
||||
if self.press_seen:
|
||||
self.press_seen = False
|
||||
self.queue.pop(0)
|
||||
if not self.open_next():
|
||||
self.finish(self.dark["id"])
|
||||
else:
|
||||
self.press_seen = True
|
||||
return 200, dict(self.dark)
|
||||
if path == "/wiz":
|
||||
return 200, {"versions": dict(self.versions), "recommended": [], "completed": True}
|
||||
if path == "/wiz/sheet.svg":
|
||||
self.previews.append(q.get("card"))
|
||||
if q.get("card") not in LIVE:
|
||||
return 404, {"error": "no such card"}
|
||||
return 200, b"<svg xmlns='http://www.w3.org/2000/svg'></svg>", "image/svg+xml"
|
||||
if path == "/wiz/sheet.gcode":
|
||||
# the text is M4 at the mark dose, as the daemon serves it; the
|
||||
# head's Z is the edge, the tail goes back to it, and the focus
|
||||
# ladder spans the window the settings hold now
|
||||
lens = lens_block(s)
|
||||
lines = ["; mock", "G0 Z%.2f" % lens["edge_z"], "M4 S400", "G0 X12 Y12", "G1 X40 Y12 F3000"]
|
||||
if q.get("card") == "laser.focus":
|
||||
lines += ["G0 Z%.2f" % z for z in ladder_z(lens["edge_z"], lens["below"], lens["above"])]
|
||||
if self.stray_z is not None:
|
||||
lines.append("G0 Z%.2f" % self.stray_z)
|
||||
lines += ["M5", "G0 Z%.2f" % lens["edge_z"], "G0 X0 Y0", "M2"]
|
||||
return 200, ("\n".join(lines) + "\n").encode(), "text/plain"
|
||||
return None
|
||||
|
||||
def on_post(self, path, form):
|
||||
s = self.fake.state["settings"]
|
||||
if path.startswith("/wiz/") and path.endswith("/start"):
|
||||
wid = path[len("/wiz/"):-len("/start")]
|
||||
if wid not in SCRIPT:
|
||||
return 404, {"error": "no such wizard"}
|
||||
self.order.append(wid)
|
||||
self.settings_at_start[wid] = dict(s)
|
||||
s.update(self.start_writes.get(wid, {}))
|
||||
self.lens_at_start = lens_block(s)
|
||||
self.queue = list(SCRIPT[wid][0])
|
||||
self.press_seen = False
|
||||
self.dark.update({"id": wid, "running": True, "phase": "starting", "result": None, "error": ""})
|
||||
self.open_next()
|
||||
return 200, {"started": True, "id": wid}
|
||||
if path.startswith("/wiz/") and path.endswith("/answer"):
|
||||
wid = path[len("/wiz/"):-len("/answer")]
|
||||
p = self.dark.get("prompt")
|
||||
if not p or form.get("seq") != str(p["seq"]):
|
||||
return 409, {"error": "no such prompt is open"}
|
||||
value = form.get("value")
|
||||
self.answers.setdefault(wid, []).append(value)
|
||||
if p["id"] == "thickness":
|
||||
imperial = (self.units or s.get("ui_units")) == "imperial"
|
||||
mm = float(value) * (25.4 if imperial else 1.0)
|
||||
if not 0 < mm <= 30:
|
||||
self.open_next() # the same prompt again, a new sequence number
|
||||
return 200, {"ok": True}
|
||||
self.thickness = mm
|
||||
self.queue.pop(0)
|
||||
if not self.open_next():
|
||||
self.finish(wid)
|
||||
return 200, {"ok": True}
|
||||
if path.startswith("/wiz/") and path.endswith("/abort"):
|
||||
self.aborted.append(path[len("/wiz/"):-len("/abort")])
|
||||
self.dark.update({"running": False, "prompt": None, "error": "aborted"})
|
||||
return 200, {"ok": True}
|
||||
if path == "/settings":
|
||||
for k, v in form.items():
|
||||
if v == "":
|
||||
s.pop(k, None)
|
||||
else:
|
||||
s[k] = v
|
||||
return 200, dict(s)
|
||||
return None
|
||||
|
||||
|
||||
class BurnRunTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.fake = helpers.FakeForgectrl().start()
|
||||
self.daemon = ScriptedDaemon(self.fake)
|
||||
self.poll = setup_dark.POLL_S
|
||||
setup_dark.POLL_S = 0.01
|
||||
|
||||
def tearDown(self):
|
||||
setup_dark.POLL_S = self.poll
|
||||
self.fake.stop()
|
||||
|
||||
def run_test(self, tid):
|
||||
t = catalog.load_suite()[tid]
|
||||
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))
|
||||
return run
|
||||
|
||||
def assert_found(self, keys=None):
|
||||
s = self.fake.state["settings"]
|
||||
for k, v in FOUND.items():
|
||||
if keys is None or k in keys:
|
||||
self.assertEqual(s.get(k), v, k)
|
||||
|
||||
def test_the_focus_burn_in_the_lens_travel_window(self):
|
||||
run = self.run_test("setup.focus")
|
||||
d = self.daemon
|
||||
self.assertEqual(d.order, ["laser.focus"])
|
||||
self.assertEqual(d.answers["laser.focus"], ["Yes", HOME, "Continue", "3.2", "Continue", "11"])
|
||||
self.assertEqual(d.previews, ["laser.focus", "laser.focus"]) # before the burn, and after it
|
||||
# a fresh head's focus: the edge cleared, the window the Lens travel check's
|
||||
start = d.settings_at_start["laser.focus"]
|
||||
self.assertNotIn("lens_hall_edge_z_mm", start)
|
||||
self.assertEqual((start["lens_stop_below_steps"], start["lens_stop_above_steps"]), ("14", "20"))
|
||||
self.assertEqual(run.evidence["result"]["emission"]["hv_max"], 610)
|
||||
fw = run.evidence["focus_window"]
|
||||
self.assertEqual(fw["window"]["below"], 14)
|
||||
self.assertEqual(fw["window"]["above"], 20)
|
||||
self.assertEqual(fw["edge"], "6.96")
|
||||
self.assertEqual(fw["program_z_before"]["reach"], [-1.44, 10.19]) # the default edge
|
||||
self.assertEqual(fw["program_z_after"]["reach"], [2.17, 13.8]) # the edge the burn wrote
|
||||
self.assertEqual(len(fw["program_z_after"]["z"]), 14) # head, twelve lines, tail
|
||||
self.assertEqual(run.evidence["settings_before"],
|
||||
{k: FOUND[k] for k in ("lens_hall_edge_z_mm", "lens_stop_below_steps",
|
||||
"lens_stop_above_steps")})
|
||||
self.assert_found() # restored
|
||||
|
||||
def test_the_focus_burn_in_the_fallback_window(self):
|
||||
# A head the Lens travel check has not measured: the settings hold
|
||||
# no window, so the ladder runs in the fallback and the burn leaves
|
||||
# the keys unset.
|
||||
s = self.fake.state["settings"]
|
||||
s.pop("lens_stop_below_steps")
|
||||
s.pop("lens_stop_above_steps")
|
||||
run = self.run_test("setup.focus")
|
||||
fw = run.evidence["focus_window"]
|
||||
self.assertEqual((fw["window"]["below"], fw["window"]["above"]), setup_dark.FALLBACK_WINDOW)
|
||||
self.assertNotIn("lens_stop_below_steps", s)
|
||||
self.assertNotIn("lens_stop_above_steps", s)
|
||||
self.assertEqual(s["lens_hall_edge_z_mm"], "4.10")
|
||||
|
||||
def test_the_floor_burn(self):
|
||||
run = self.run_test("setup.floor")
|
||||
d = self.daemon
|
||||
self.assertEqual(d.order, ["laser.floor"])
|
||||
self.assertEqual(d.answers["laser.floor"], ["Yes", HOME, "Continue", "3.2", "Continue", "8"])
|
||||
self.assertEqual(d.previews, ["laser.floor"])
|
||||
self.assertEqual(run.evidence["result"]["floor_density"], 10.0)
|
||||
self.assertEqual(run.evidence["settings_before"], {"laser_floor_density": "12"})
|
||||
self.assert_found()
|
||||
|
||||
def test_imperial_units_answer_the_thickness_in_inches(self):
|
||||
# The number prompts read ui_units: on an imperial machine the
|
||||
# thickness is 0.125 in and the record carries 3.175 mm.
|
||||
self.fake.state["settings"]["ui_units"] = "imperial"
|
||||
run = self.run_test("setup.floor")
|
||||
self.assertEqual(self.daemon.answers["laser.floor"][3], "0.125")
|
||||
self.assertEqual(run.evidence["thickness_answer"], "0.125")
|
||||
self.assertEqual(run.evidence["result"]["thickness_mm"], 3.175)
|
||||
|
||||
def test_a_thickness_the_burn_refuses_ends_it_before_anything_fires(self):
|
||||
# 3.2 read as inches is over the 30 mm cap, and the burn asks again:
|
||||
# the test ends the burn rather than answer the same thing forever.
|
||||
self.daemon.units = "imperial"
|
||||
with self.assertRaises(Failed) as cm:
|
||||
self.run_test("setup.floor")
|
||||
self.assertIn("asked thickness again", str(cm.exception))
|
||||
self.assertEqual(self.daemon.answers["laser.floor"], ["Yes", HOME, "Continue", "3.2"])
|
||||
self.assertEqual(self.daemon.aborted, ["laser.floor"])
|
||||
self.assert_found()
|
||||
|
||||
def test_a_prompt_the_test_does_not_answer_ends_the_burn(self):
|
||||
# The motors found released: the burn asks for the head pushed home
|
||||
# by hand, which the bench never needs.
|
||||
SCRIPT["laser.floor"][0].insert(1, ("home-push", "continue", ["Continue"]))
|
||||
try:
|
||||
with self.assertRaises(Failed) as cm:
|
||||
self.run_test("setup.floor")
|
||||
finally:
|
||||
SCRIPT["laser.floor"][0].pop(1)
|
||||
self.assertIn("asked home-push, which the test does not answer", str(cm.exception))
|
||||
self.assertEqual(self.daemon.aborted, ["laser.floor"])
|
||||
self.assert_found()
|
||||
|
||||
def test_a_dark_burn_fails_and_still_restores(self):
|
||||
self.daemon.patch["laser.floor"] = {"emission": {"hv_max": 0, "laser_on_samples": 0, "thermopile_delta": 0}}
|
||||
with self.assertRaises(Failed) as cm:
|
||||
self.run_test("setup.floor")
|
||||
self.assertIn("the tube current never rose", str(cm.exception))
|
||||
self.assert_found()
|
||||
|
||||
def test_a_focus_burn_that_writes_the_window_fails(self):
|
||||
# The focus burn writes the edge alone: the window is the Lens
|
||||
# travel check's. A burn that moves it fails, and it goes back.
|
||||
self.daemon.start_writes["laser.focus"] = {"lens_stop_below_steps": "16", "lens_stop_above_steps": "22"}
|
||||
with self.assertRaises(Failed) as cm:
|
||||
self.run_test("setup.focus")
|
||||
self.assertIn("the focus burn wrote the lens window", str(cm.exception))
|
||||
self.assert_found()
|
||||
|
||||
def test_a_focus_ladder_outside_the_window_held_fails(self):
|
||||
self.daemon.patch["laser.focus"] = {"window": {"below": 10, "above": 12, "reach_min": -0.07,
|
||||
"reach_max": 7.46}}
|
||||
with self.assertRaises(Failed) as cm:
|
||||
self.run_test("setup.focus")
|
||||
self.assertIn("the ladder ran in 10/12", str(cm.exception))
|
||||
self.assert_found()
|
||||
|
||||
def test_a_focus_burn_that_reports_a_stop_search_fails(self):
|
||||
self.daemon.patch["laser.focus"] = {"stops": {"found": True, "below": 14, "above": 20}}
|
||||
with self.assertRaises(Failed) as cm:
|
||||
self.run_test("setup.focus")
|
||||
self.assertIn("searched for the stops", str(cm.exception))
|
||||
self.assert_found()
|
||||
|
||||
def test_a_program_z_beyond_the_reach_fails_before_the_burn(self):
|
||||
# A served program that commands a Z the lens does not reach is
|
||||
# refused before the burn starts: nothing burns.
|
||||
self.daemon.stray_z = 12.6
|
||||
with self.assertRaises(Failed) as cm:
|
||||
self.run_test("setup.floor")
|
||||
self.assertIn("outside the lens window", str(cm.exception))
|
||||
self.assertIn("12.6", str(cm.exception))
|
||||
self.assertEqual(self.daemon.order, [])
|
||||
self.assert_found()
|
||||
|
||||
def test_the_result_keys_the_tests_require(self):
|
||||
# every key either test reads is one the scripted burn reports
|
||||
self.assertTrue(set(setup_burns.FLOOR_KEYS) <= set(SCRIPT["laser.floor"][1]) | {"thickness_mm"})
|
||||
self.assertTrue(set(setup_burns.FOCUS_KEYS)
|
||||
<= set(SCRIPT["laser.focus"][1]) | {"thickness_mm", "window", "ladder_z"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -3,11 +3,13 @@
|
||||
# https://community.openglow.org
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""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, 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."""
|
||||
"""The setup's checks in the catalog: registered as automatic tests, the
|
||||
takeover ones marked so, every covers map naming the check runner, and
|
||||
the two that put the intake fan count back as found refused unless it is
|
||||
set; and the check driver itself against the fake daemon: a 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 or put back
|
||||
as found."""
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
@@ -17,8 +19,11 @@ import helpers # noqa: E402
|
||||
from forgetest import catalog # noqa: E402
|
||||
from forgetest.runner import Context, Run # noqa: E402
|
||||
|
||||
IDS = ["setup.check-switches", "setup.check-sensors", "setup.check-airflow",
|
||||
"setup.check-cameras", "setup.check-motion", "setup.check-flow-verify"]
|
||||
IDS = ["setup.check-switches", "setup.check-sensors", "setup.check-airflow", "setup.check-intakes",
|
||||
"setup.check-cameras", "setup.check-motion", "setup.check-lens", "setup.check-flow-verify"]
|
||||
# The checks that put the intake fan count back as found: they start only
|
||||
# with it set, so a put-back never leaves the gate closed behind them.
|
||||
COUNTED = ("setup.check-airflow", "setup.check-intakes")
|
||||
|
||||
|
||||
class RegistrationTests(unittest.TestCase):
|
||||
@@ -33,18 +38,42 @@ class RegistrationTests(unittest.TestCase):
|
||||
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 ("setup.check-sensors", "setup.check-airflow", "setup.check-motion",
|
||||
"setup.check-flow-verify"):
|
||||
for tid in ("setup.check-sensors", "setup.check-airflow", "setup.check-intakes",
|
||||
"setup.check-motion", "setup.check-lens", "setup.check-flow-verify"):
|
||||
self.assertEqual(self.reg[tid].kind, "auto", tid)
|
||||
|
||||
def test_no_check_asks_the_operator_to_prepare_anything(self):
|
||||
for tid in IDS:
|
||||
self.assertIsNone(getattr(self.reg[tid], "precheck", None), tid)
|
||||
if tid not in COUNTED:
|
||||
self.assertIsNone(getattr(self.reg[tid], "precheck", None), tid)
|
||||
|
||||
def test_the_intake_count_must_be_set_before_a_check_puts_it_back(self):
|
||||
fake = helpers.FakeForgectrl().start()
|
||||
try:
|
||||
for tid in COUNTED:
|
||||
fake.state["settings"].pop("cool_intake_fans", None)
|
||||
why = self.reg[tid].cannot_start()
|
||||
self.assertIn("cool_intake_fans is unset", why, tid)
|
||||
self.assertIn("Intake fans step", why, tid)
|
||||
fake.state["settings"]["cool_intake_fans"] = "2"
|
||||
self.assertIsNone(self.reg[tid].cannot_start(), tid)
|
||||
finally:
|
||||
fake.stop()
|
||||
|
||||
def test_the_checks_that_take_the_machine_say_so(self):
|
||||
for tid in ("setup.check-airflow", "setup.check-motion", "setup.check-flow-verify"):
|
||||
for tid in ("setup.check-airflow", "setup.check-intakes", "setup.check-motion", "setup.check-lens",
|
||||
"setup.check-flow-verify"):
|
||||
self.assertEqual(self.reg[tid].hardware, "takeover", tid)
|
||||
|
||||
def test_the_lens_check_covers_the_finder_and_the_frame(self):
|
||||
covers = set(self.reg["setup.check-lens"].covers)
|
||||
for want in (("forgectrl", "src/lensfind.*"), ("forgectrl", "src/lens.*")):
|
||||
self.assertIn(want, covers)
|
||||
self.assertIn("setup.check-motion", self.reg["setup.check-lens"].requires)
|
||||
covers = set(self.reg["setup.check-intakes"].covers)
|
||||
for want in (("forgectrl", "src/cool.c"), ("forgectrl", "src/airflow.*")):
|
||||
self.assertIn(want, covers)
|
||||
|
||||
def test_every_check_covers_the_runner_and_the_page(self):
|
||||
for tid in IDS:
|
||||
covers = set(self.reg[tid].covers)
|
||||
@@ -256,5 +285,184 @@ class AirflowCheckTests(unittest.TestCase):
|
||||
self.assert_put_back()
|
||||
|
||||
|
||||
|
||||
class ScriptedCheck(unittest.TestCase):
|
||||
"""A dark check against the fake daemon: the start, one poll that sees
|
||||
the run, the next that sees its end (`finish` writes the settings and
|
||||
returns the result), the record at version 1, and /status's lens block
|
||||
read from the settings as the daemon reads them."""
|
||||
|
||||
WID = ""
|
||||
|
||||
def setUp(self):
|
||||
self.fake = helpers.FakeForgectrl().start()
|
||||
self.dark = {"id": "", "running": False, "phase": "", "progress": 0, "elapsed_s": 0,
|
||||
"log": [], "prompt": None, "result": None, "error": "",
|
||||
"shots": {"lid": False, "head": False}}
|
||||
self.versions = {}
|
||||
self.polls = 0
|
||||
self.prompt = None # a prompt the run opens on its first poll, when set
|
||||
self.answers = []
|
||||
|
||||
def on_get(path, q):
|
||||
s = self.fake.state["settings"]
|
||||
if path == "/wiz/dark":
|
||||
if self.dark["running"]:
|
||||
self.polls += 1
|
||||
if self.polls == 1 and self.prompt:
|
||||
self.dark["prompt"] = dict(self.prompt, seq=1)
|
||||
elif self.polls >= 2 and not self.dark["prompt"]:
|
||||
self.dark.update({"running": False, "progress": 100, "result": self.finish(s)})
|
||||
self.versions[self.WID] = 1
|
||||
return 200, dict(self.dark)
|
||||
if path == "/wiz":
|
||||
return 200, {"versions": dict(self.versions), "recommended": [], "completed": True}
|
||||
if path == "/status":
|
||||
def count(key, fallback):
|
||||
v = s.get(key)
|
||||
return int(v) if v and v.isdigit() and 1 <= int(v) <= 40 else fallback
|
||||
lens = {"below": count("lens_stop_below_steps", 10), "above": count("lens_stop_above_steps", 12)}
|
||||
return 200, dict(self.fake.state["status"], lens=lens)
|
||||
return None
|
||||
|
||||
def on_post(path, form):
|
||||
s = self.fake.state["settings"]
|
||||
if path == "/wiz/%s/start" % self.WID:
|
||||
self.dark.update({"id": self.WID, "running": True, "phase": "running", "prompt": None})
|
||||
return 200, {"started": True, "id": self.WID}
|
||||
if path == "/wiz/%s/answer" % self.WID:
|
||||
self.answers.append(dict(form))
|
||||
self.dark["prompt"] = None
|
||||
return 200, {"ok": True}
|
||||
if path == "/wiz/%s/abort" % self.WID:
|
||||
self.dark.update({"running": False, "prompt": None, "error": "aborted"})
|
||||
return 200, {"ok": True}
|
||||
if path == "/settings":
|
||||
for k, v in form.items():
|
||||
if v == "":
|
||||
s.pop(k, None)
|
||||
else:
|
||||
s[k] = v
|
||||
return 200, dict(s)
|
||||
return None
|
||||
self.fake.on_get = on_get
|
||||
self.fake.on_post = on_post
|
||||
|
||||
def tearDown(self):
|
||||
self.fake.stop()
|
||||
|
||||
def run_check(self, tid):
|
||||
t = catalog.load_suite()[tid]
|
||||
run = Run("test", t.id, t.title)
|
||||
t.fn(Context(run, None, t))
|
||||
return run
|
||||
|
||||
|
||||
class LensCheckTests(ScriptedCheck):
|
||||
"""setup.check-lens's judge: the window is the stops found or the
|
||||
fallback, the settings and /status hold it, and both keys go back as
|
||||
found."""
|
||||
|
||||
WID = "lens.travel"
|
||||
FOUND = {"lens_stop_below_steps": "12", "lens_stop_above_steps": "18"}
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.fake.state["settings"].update(self.FOUND)
|
||||
self.stops = {"found": True, "below": 14, "above": 20, "contact_below": 16, "contact_above": 22, "why": ""}
|
||||
self.writes = None # what the check writes; None = the window of its result
|
||||
|
||||
def finish(self, s):
|
||||
st = self.stops
|
||||
below, above = (st["below"], st["above"]) if st["found"] else (10, 12)
|
||||
w = self.writes or (below, above)
|
||||
s.update({"lens_stop_below_steps": str(w[0]), "lens_stop_above_steps": str(w[1])})
|
||||
return {"z_passes": [100, 101], "stops": dict(st),
|
||||
"window": {"below": below, "above": above, "found": st["found"]}, "summary": "x"}
|
||||
|
||||
def assert_put_back(self):
|
||||
s = self.fake.state["settings"]
|
||||
for k, v in self.FOUND.items():
|
||||
self.assertEqual(s.get(k), v, k)
|
||||
|
||||
def test_the_stops_found_are_the_window(self):
|
||||
run = self.run_check("setup.check-lens")
|
||||
self.assertEqual(run.evidence["window"]["settings"], ("14", "20"))
|
||||
self.assertEqual(run.evidence["window"]["status"]["below"], 14)
|
||||
self.assertEqual(run.evidence["result"]["stops"]["contact_above"], 22)
|
||||
self.assert_put_back()
|
||||
|
||||
def test_stops_not_found_write_the_fallback(self):
|
||||
self.stops = {"found": False, "below": 0, "above": 0, "contact_below": 0, "contact_above": 0,
|
||||
"why": "the lens steps do not ring clearly enough on this head"}
|
||||
run = self.run_check("setup.check-lens")
|
||||
self.assertEqual(run.evidence["window"]["settings"], ("10", "12"))
|
||||
self.assert_put_back()
|
||||
|
||||
def test_a_window_the_settings_do_not_hold_fails_and_still_restores(self):
|
||||
self.writes = (12, 18)
|
||||
with self.assertRaises(Exception) as cm:
|
||||
self.run_check("setup.check-lens")
|
||||
self.assertIn("the settings hold", str(cm.exception))
|
||||
self.assert_put_back()
|
||||
|
||||
def test_a_question_fails_the_check(self):
|
||||
self.prompt = {"id": "lens-stop", "kind": "continue", "text": "?", "options": ["Continue"]}
|
||||
with self.assertRaises(Exception) as cm:
|
||||
self.run_check("setup.check-lens")
|
||||
self.assertIn("asked a question", str(cm.exception))
|
||||
self.assertEqual(self.answers, [])
|
||||
self.assertIn(("/wiz/lens.travel/abort", {}), self.fake.posts)
|
||||
self.assert_put_back()
|
||||
|
||||
|
||||
class IntakesCheckTests(ScriptedCheck):
|
||||
"""setup.check-intakes's judge: the tachs count the intake fans the
|
||||
machine held, the step writes the count, and it goes back as found."""
|
||||
|
||||
WID = "airflow.intakes"
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.fake.state["settings"]["cool_intake_fans"] = "2"
|
||||
self.count = 2
|
||||
|
||||
def finish(self, s):
|
||||
s["cool_intake_fans"] = str(self.count)
|
||||
return {"intake_fans": self.count, "intake_1_rpm": 4150.0,
|
||||
"intake_2_rpm": 0.0 if self.count == 1 else 4100.0, "intake_2_silent": self.count == 1,
|
||||
"counted_by": "tachs", "summary": "x"}
|
||||
|
||||
def test_two_intake_fans_counted_and_put_back(self):
|
||||
run = self.run_check("setup.check-intakes")
|
||||
self.assertEqual(run.evidence["count"]["counted"], 2)
|
||||
self.assertEqual(self.fake.state["settings"]["cool_intake_fans"], "2")
|
||||
self.assertIn(("/settings", {"cool_intake_fans": "2"}), self.fake.posts) # the put-back
|
||||
|
||||
def test_one_intake_fan_on_a_machine_built_with_one(self):
|
||||
self.fake.state["settings"]["cool_intake_fans"] = "1"
|
||||
self.count = 1
|
||||
run = self.run_check("setup.check-intakes")
|
||||
self.assertEqual(run.evidence["count"]["counted"], 1)
|
||||
self.assertEqual(self.fake.state["settings"]["cool_intake_fans"], "1")
|
||||
|
||||
def test_a_count_other_than_the_machine_held_fails_and_puts_it_back(self):
|
||||
self.count = 1
|
||||
with self.assertRaises(Exception) as cm:
|
||||
self.run_check("setup.check-intakes")
|
||||
self.assertIn("the tachs count 1 intake fans, the machine held 2", str(cm.exception))
|
||||
self.assertEqual(self.fake.state["settings"]["cool_intake_fans"], "2")
|
||||
|
||||
def test_the_operator_question_fails_the_check(self):
|
||||
# intake 1 never came up: the step asks a person to count, which
|
||||
# is not a count by the tachs
|
||||
self.prompt = {"id": "intake-count", "kind": "choice", "text": "how many?", "options": ["One", "Two"]}
|
||||
with self.assertRaises(Exception) as cm:
|
||||
self.run_check("setup.check-intakes")
|
||||
self.assertIn("intake 1 did not come up to speed", str(cm.exception))
|
||||
self.assertEqual(self.answers, [])
|
||||
self.assertEqual(self.fake.state["settings"]["cool_intake_fans"], "2")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,385 +0,0 @@
|
||||
# Copyright 2026 514 LLC d/b/a OpenGlow
|
||||
# Written by Scott Wiederhold
|
||||
# https://community.openglow.org
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""The setup's sheet in the catalog: one live test on the button with a
|
||||
piece of wood at hand, its covers map naming the live runner, the
|
||||
renderer, the sender, the page, and the driver's laser module; and the
|
||||
whole run against a scripted daemon: the placement's jogs and origin,
|
||||
then each card's preview and program read, its start, the arm prompt and
|
||||
the pick answered, the witnesses judged, the record read at version 1,
|
||||
every card's evidence filed under its own key, and the settings the cards
|
||||
wrote put back as found at the end."""
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
import helpers # noqa: E402
|
||||
from forgetest import catalog # noqa: E402
|
||||
from forgetest.runner import Context, Run # noqa: E402
|
||||
from forgetest.suite import 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
|
||||
# must then lie in the window the card wrote.
|
||||
PREVIEWS = ["sheet.frame", "laser.focus", "laser.focus", "laser.floor", "laser.dose-curve", "laser.corner",
|
||||
"cooling.flow-load"]
|
||||
EMISSION = {"hv_max": 610, "laser_on_samples": 900, "thermopile_delta": 600, "lit_s": 30.0}
|
||||
SPM = 2.922 # the lens screw, half-steps per mm ($102)
|
||||
EDGE_DEFAULT = 3.35
|
||||
|
||||
|
||||
def lens_block(s):
|
||||
"""The /status lens block as the daemon builds it from the settings: a
|
||||
stop count from 1 to 40 stands, anything else is the fallback."""
|
||||
def count(key, fallback):
|
||||
try:
|
||||
n = float(s.get(key, ""))
|
||||
except ValueError:
|
||||
return fallback, False
|
||||
return (int(n), True) if 1 <= n <= 40 else (fallback, False)
|
||||
try:
|
||||
edge = float(s.get("lens_hall_edge_z_mm", ""))
|
||||
except ValueError:
|
||||
edge = EDGE_DEFAULT
|
||||
below, a = count("lens_stop_below_steps", 10)
|
||||
above, b = count("lens_stop_above_steps", 12)
|
||||
return {"edge_z": round(edge, 2), "below": below, "above": above, "stops_found": a and b,
|
||||
"reach_min": round(edge - below / SPM, 2), "reach_max": round(edge + above / SPM, 2)}
|
||||
|
||||
|
||||
def ladder_z(edge, below, above):
|
||||
"""The focus ladder the daemon builds: twelve whole half-steps from
|
||||
the top of the window to the bottom."""
|
||||
return [round(edge + round(above - (above + below) * i / 11) / SPM, 2) for i in range(12)]
|
||||
|
||||
|
||||
# The window the focus card finds and writes before its controller starts.
|
||||
STOPS = {"lens_stop_below_steps": "14", "lens_stop_above_steps": "20"}
|
||||
FOCUS_WINDOW = {"below": 14, "above": 20, "reach_min": round(EDGE_DEFAULT - 14 / SPM, 2),
|
||||
"reach_max": round(EDGE_DEFAULT + 20 / SPM, 2)}
|
||||
|
||||
# Each wizard as the daemon runs it: its prompts in order (id, kind,
|
||||
# options) and the result it ends with. A "jog" prompt repeats until the
|
||||
# origin is set.
|
||||
SCRIPT = {
|
||||
"sheet.place": ([("place", "jog", ["X-10", "X+10", "Set origin"]),
|
||||
("sheet-kind", "choice", ["Full sheet", "One card"]),
|
||||
("thickness", "number", ["Set", "Skip"])],
|
||||
{"origin_x": 100, "origin_y": 200, "origin_z": 0, "steps_per_mm": {"x": 40.0, "y": 40.0},
|
||||
"z_referenced": True, "thickness_mm": 3.2, "alone": False}),
|
||||
"sheet.frame": ([("frame-arm", "continue", ["Continue"]), ("frame-ok", "choice", ["Yes", "No", "Again"])],
|
||||
{"mark_s": 400, "mark_feed": 3000, "emission": EMISSION}),
|
||||
"laser.focus": ([("focus-arm", "continue", ["Continue"]),
|
||||
("focus-pick", "multichoice", [str(i) for i in range(1, 13)]),
|
||||
("thickness", "number", ["Keep", "Set"])],
|
||||
{"pick": 11, "thickness_mm": 3.2, "pick_half_steps": -11, "edge_z_mm": 6.96,
|
||||
"steps_per_mm": 2.922, "max_height_mm": 13.81,
|
||||
"focus_range_mm": {"min": 2.17, "max": 13.81},
|
||||
"stops": {"found": True, "below": 14, "above": 20, "contact_below": 16,
|
||||
"contact_above": 22, "why": ""},
|
||||
"window": dict(FOCUS_WINDOW), "ladder_z": ladder_z(EDGE_DEFAULT, 14, 20),
|
||||
"emission": EMISSION}),
|
||||
"laser.floor": ([("floor-arm", "continue", ["Continue"]),
|
||||
("floor-pick", "choice", ["2", "4", "6", "8", "None"])],
|
||||
{"faintest_density": 8.0, "floor_density": 10.0, "emission": EMISSION}),
|
||||
"laser.dose-curve": ([("dose-arm", "continue", ["Continue"])],
|
||||
{"points": [[10, 0.44], [100, 100]], "curve": "10:0.44,100:100", "emission": EMISSION}),
|
||||
"laser.corner": ([("corner-arm", "continue", ["Continue"]),
|
||||
("corner-pick", "choice", ["1.00", "1.25", "1.50", "1.75", "2.00"])],
|
||||
{"gamma": 1.5, "emission": EMISSION}),
|
||||
"cooling.flow-load": ([("load-arm", "continue", ["Continue"])],
|
||||
{"lit_s": 30.0, "dose_raw_s": 12000, "peak_c": 0.72, "k_density": 2.14e-5,
|
||||
"k_cw": 2.78e-5, "emission": EMISSION}),
|
||||
}
|
||||
# What a card writes when it starts (the focus card's window, before its
|
||||
# controller starts) and when it ends.
|
||||
START_WRITES = {"laser.focus": dict(STOPS)}
|
||||
WRITES = {
|
||||
"laser.focus": {"lens_hall_edge_z_mm": "6.96"},
|
||||
"laser.floor": {"laser_floor_density": "10"},
|
||||
"laser.dose-curve": {"laser_dose_curve": "10:0.44,100:100"},
|
||||
"laser.corner": {"laser_corner_gamma": "1.5"},
|
||||
"cooling.flow-load": {"cool_laser_heat_density": "2.14e-5", "cool_laser_heat_cw": "2.78e-5"},
|
||||
}
|
||||
FOUND = {"lens_hall_edge_z_mm": "3.35", "lens_stop_below_steps": "10", "lens_stop_above_steps": "12",
|
||||
"laser_floor_density": "12",
|
||||
"laser_dose_curve": "off", "laser_corner_gamma": "2", "cool_laser_heat_density": "1e-5",
|
||||
"cool_laser_heat_cw": "1e-5"}
|
||||
|
||||
|
||||
class RegistrationTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.t = catalog.load_suite()["setup.sheet"]
|
||||
|
||||
def test_one_live_test_on_the_button_with_the_sheet_at_hand(self):
|
||||
t = self.t
|
||||
self.assertEqual(t.kind, "live")
|
||||
self.assertEqual(t.mode, "grbl")
|
||||
self.assertEqual(t.hardware, "takeover")
|
||||
self.assertIn("button", t.actions)
|
||||
self.assertTrue(t.hands)
|
||||
self.assertFalse(t.fixture_runnable(("button", "lid", "interlock")))
|
||||
self.assertIsNone(getattr(t, "precheck", None))
|
||||
for want in ("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("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)
|
||||
for want in (("forgectrl", "src/wizlive.*"), ("forgectrl", "src/sheet.*"), ("forgectrl", "src/jobstream.*"),
|
||||
("forgectrl", "src/curverec.*"), ("forgectrl", "src/font_hershey.*"),
|
||||
("forgectrl", "src/ui/wizard.*"), ("forgectrl", "src/wizrun.h"),
|
||||
("grblhal-glowforge", "src/glowforge_laser.c")):
|
||||
self.assertIn(want, covers, "lacks %s" % (want,))
|
||||
|
||||
def test_the_steps_ask_for_one_press_and_no_page(self):
|
||||
text = " ".join(self.t.steps).lower()
|
||||
self.assertIn("press the machine's button once", text)
|
||||
self.assertNotIn("when the page", text)
|
||||
self.assertEqual(len(setup_sheet.CARDS), 6)
|
||||
|
||||
|
||||
class ScriptedDaemon:
|
||||
"""The wizard runner as the suite sees it: /wiz/<id>/start opens the
|
||||
first prompt, each answer opens the next or ends the wizard with its
|
||||
result and its writes, /wiz/dark reports it, GET /wiz carries the
|
||||
versions, the sheet preview and program are served per card."""
|
||||
|
||||
def __init__(self, fake):
|
||||
self.fake = fake
|
||||
fake.state["settings"].update(FOUND)
|
||||
self.dark = {"id": "", "running": False, "phase": "", "progress": 0, "elapsed_s": 0,
|
||||
"log": [], "prompt": None, "result": None, "error": "",
|
||||
"shots": {"lid": False, "head": False}}
|
||||
self.versions = {}
|
||||
self.answers = {} # wid -> [values]
|
||||
self.order = [] # the wizards as started
|
||||
self.settings_at_start = {} # wid -> the settings as the wizard found them
|
||||
self.previews = []
|
||||
self.queue = []
|
||||
self.seq = 0
|
||||
self.stray_z = None # a Z the served program commands beyond the reach, when set
|
||||
fake.on_get = self.on_get
|
||||
fake.on_post = self.on_post
|
||||
|
||||
def open_next(self):
|
||||
if not self.queue:
|
||||
return False
|
||||
pid, kind, options = self.queue[0]
|
||||
self.seq += 1
|
||||
self.dark["prompt"] = {"seq": self.seq, "id": pid, "kind": kind, "text": pid, "options": options}
|
||||
return True
|
||||
|
||||
def on_get(self, path, q):
|
||||
s = self.fake.state["settings"]
|
||||
if path == "/status":
|
||||
# the lens block follows the settings, as the daemon's does
|
||||
return 200, dict(self.fake.state["status"], lens=lens_block(s))
|
||||
if path == "/wiz/dark":
|
||||
return 200, dict(self.dark)
|
||||
if path == "/wiz":
|
||||
return 200, {"versions": dict(self.versions), "required": [], "completed": True}
|
||||
if path == "/wiz/sheet.svg":
|
||||
self.previews.append(q.get("card"))
|
||||
if q.get("card") not in LIVE:
|
||||
return 404, {"error": "no such card"}
|
||||
return 200, b"<svg xmlns='http://www.w3.org/2000/svg'></svg>", "image/svg+xml"
|
||||
if path == "/wiz/sheet.gcode":
|
||||
# the frame and the text are M4 at the mark dose, as the daemon
|
||||
# serves them; the head's Z is the edge, the tail goes back to
|
||||
# it, and the focus card's ladder spans the window the settings
|
||||
# hold now
|
||||
lens = lens_block(s)
|
||||
lines = ["; mock", "G0 Z%.2f" % lens["edge_z"], "M4 S400", "G0 X60 Y42", "G1 X120 Y42 F3000"]
|
||||
if q.get("card") == "laser.focus":
|
||||
lines += ["G0 Z%.2f" % z for z in ladder_z(lens["edge_z"], lens["below"], lens["above"])]
|
||||
if self.stray_z is not None:
|
||||
lines.append("G0 Z%.2f" % self.stray_z)
|
||||
lines += ["M5", "G0 Z%.2f" % lens["edge_z"], "G0 X0 Y0", "M2"]
|
||||
return 200, ("\n".join(lines) + "\n").encode(), "text/plain"
|
||||
return None
|
||||
|
||||
def on_post(self, path, form):
|
||||
s = self.fake.state["settings"]
|
||||
if path.startswith("/wiz/") and path.endswith("/start"):
|
||||
wid = path[len("/wiz/"):-len("/start")]
|
||||
if wid not in SCRIPT:
|
||||
return 404, {"error": "no such wizard"}
|
||||
self.order.append(wid)
|
||||
self.settings_at_start[wid] = dict(s)
|
||||
s.update(START_WRITES.get(wid, {}))
|
||||
self.queue = list(SCRIPT[wid][0])
|
||||
self.dark.update({"id": wid, "running": True, "phase": "starting", "result": None, "error": ""})
|
||||
self.open_next()
|
||||
return 200, {"started": True, "id": wid}
|
||||
if path.startswith("/wiz/") and path.endswith("/answer"):
|
||||
wid = path[len("/wiz/"):-len("/answer")]
|
||||
p = self.dark.get("prompt")
|
||||
if not p or form.get("seq") != str(p["seq"]):
|
||||
return 409, {"error": "no such prompt is open"}
|
||||
self.answers.setdefault(wid, []).append(form.get("value"))
|
||||
if not (p["kind"] == "jog" and form.get("value") != "Set origin"):
|
||||
self.queue.pop(0)
|
||||
if not self.open_next():
|
||||
s.update(WRITES.get(wid, {}))
|
||||
self.dark.update({"running": False, "prompt": None, "progress": 100,
|
||||
"result": dict(SCRIPT[wid][1])})
|
||||
self.versions[wid] = 1
|
||||
return 200, {"ok": True}
|
||||
if path == "/settings":
|
||||
for k, v in form.items():
|
||||
if v == "":
|
||||
s.pop(k, None)
|
||||
else:
|
||||
s[k] = v
|
||||
return 200, dict(s)
|
||||
return None
|
||||
|
||||
|
||||
class SheetRunTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.fake = helpers.FakeForgectrl().start()
|
||||
self.daemon = ScriptedDaemon(self.fake)
|
||||
|
||||
def tearDown(self):
|
||||
self.fake.stop()
|
||||
|
||||
def test_the_whole_sheet_on_one_piece(self):
|
||||
t = catalog.load_suite()["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))
|
||||
d = self.daemon
|
||||
self.assertEqual(d.order, ["sheet.place"] + LIVE)
|
||||
self.assertEqual(d.answers["sheet.place"], ["X+10", "X-10", "Set origin", "Full sheet", "3.2"])
|
||||
self.assertEqual(d.answers["sheet.frame"], ["Continue", "Yes"])
|
||||
self.assertEqual(d.answers["laser.focus"], ["Continue", "11", "Keep"])
|
||||
self.assertEqual(d.answers["laser.floor"], ["Continue", "8"])
|
||||
self.assertEqual(d.answers["laser.corner"], ["Continue", "1.50"])
|
||||
self.assertEqual(d.previews, PREVIEWS)
|
||||
cards = run.evidence["cards"]
|
||||
self.assertEqual(sorted(cards), sorted(["sheet.place"] + LIVE))
|
||||
for wid in LIVE:
|
||||
self.assertEqual(cards[wid]["emission"]["hv_max"], 610, wid)
|
||||
self.assertEqual(cards[wid]["result"], SCRIPT[wid][1], wid)
|
||||
self.assertNotIn("emission", run.evidence)
|
||||
self.assertEqual(cards["sheet.place"]["result"]["origin_x"], 100)
|
||||
self.assertEqual(run.evidence["settings_before"], FOUND)
|
||||
# A fresh machine's run: the lens settings were gone before the
|
||||
# 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 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 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)
|
||||
self.assertEqual(fw["settings"]["lens_stop_below_steps"], "14")
|
||||
self.assertEqual(len(fw["ladder_z"]), 12)
|
||||
self.assertEqual(fw["program_z_before"]["reach"], [-0.07, 7.46]) # the fallback ladder
|
||||
self.assertEqual(fw["program_z_after"]["reach"], [2.17, 13.8]) # the written window at the new edge
|
||||
self.assertEqual(len(fw["program_z_after"]["z"]), 14) # head, twelve lines, tail
|
||||
for k, v in FOUND.items():
|
||||
self.assertEqual(self.fake.state["settings"][k], v, k) # restored
|
||||
|
||||
def test_imperial_units_answer_the_thickness_in_inches(self):
|
||||
# The number prompts read ui_units: on an imperial machine the
|
||||
# placement's thickness is 0.125 in and the record carries 3.175 mm.
|
||||
self.fake.state["settings"]["ui_units"] = "imperial"
|
||||
place_prompts, place_result = SCRIPT["sheet.place"]
|
||||
SCRIPT["sheet.place"] = (place_prompts, dict(place_result, thickness_mm=3.175))
|
||||
try:
|
||||
t = catalog.load_suite()["setup.sheet"]
|
||||
run = Run("test", t.id, t.title)
|
||||
run.unattended = True
|
||||
t.fn(Context(run, None, t))
|
||||
self.assertEqual(self.daemon.answers["sheet.place"][-1], "0.125")
|
||||
self.assertEqual(run.evidence["thickness_answer"], "0.125")
|
||||
finally:
|
||||
SCRIPT["sheet.place"] = (place_prompts, place_result)
|
||||
|
||||
def test_a_metric_answer_on_an_imperial_machine_fails_the_placement(self):
|
||||
# The defect the bench found: 3.2 read as inches is over the 30 mm
|
||||
# cap and the wizard records 0; the test must not read that as a pass.
|
||||
from forgetest.runner import Failed
|
||||
place_prompts, place_result = SCRIPT["sheet.place"]
|
||||
SCRIPT["sheet.place"] = (place_prompts, dict(place_result, thickness_mm=0.0))
|
||||
try:
|
||||
t = catalog.load_suite()["setup.sheet"]
|
||||
run = Run("test", t.id, t.title)
|
||||
run.unattended = True
|
||||
with self.assertRaises(Failed) as cm:
|
||||
t.fn(Context(run, None, t))
|
||||
self.assertIn("thickness 0.0 mm", str(cm.exception))
|
||||
self.assertEqual(self.daemon.order, ["sheet.place"]) # nothing burned
|
||||
finally:
|
||||
SCRIPT["sheet.place"] = (place_prompts, place_result)
|
||||
|
||||
def test_a_dark_burn_fails_the_run_and_still_restores(self):
|
||||
from forgetest.runner import Failed
|
||||
SCRIPT_DARK = dict(SCRIPT["laser.floor"][1])
|
||||
SCRIPT_DARK["emission"] = {"hv_max": 0, "laser_on_samples": 0, "thermopile_delta": 0}
|
||||
SCRIPT["laser.floor"] = (SCRIPT["laser.floor"][0], SCRIPT_DARK)
|
||||
try:
|
||||
t = catalog.load_suite()["setup.sheet"]
|
||||
run = Run("test", t.id, t.title)
|
||||
run.unattended = True
|
||||
with self.assertRaises(Failed):
|
||||
t.fn(Context(run, None, t))
|
||||
self.assertEqual(self.daemon.order, ["sheet.place", "sheet.frame", "laser.focus", "laser.floor"])
|
||||
for k, v in FOUND.items():
|
||||
self.assertEqual(self.fake.state["settings"][k], v, k)
|
||||
finally:
|
||||
SCRIPT["laser.floor"] = (SCRIPT["laser.floor"][0], dict(SCRIPT_DARK, emission=EMISSION))
|
||||
|
||||
def test_a_focus_window_the_settings_do_not_hold_fails_the_run(self):
|
||||
# The defect the bench found: the focus card burned its ladder over
|
||||
# the stops it found while the controller's Z limit stood on the
|
||||
# settings, which did not hold them. A daemon whose focus result
|
||||
# names a window the settings do not hold fails the run before the
|
||||
# floor card, and the settings still go back.
|
||||
from forgetest.runner import Failed
|
||||
saved = dict(START_WRITES["laser.focus"])
|
||||
START_WRITES["laser.focus"] = {}
|
||||
try:
|
||||
t = catalog.load_suite()["setup.sheet"]
|
||||
run = Run("test", t.id, t.title)
|
||||
run.unattended = True
|
||||
with self.assertRaises(Failed) as cm:
|
||||
t.fn(Context(run, None, t))
|
||||
self.assertIn("the settings hold", str(cm.exception))
|
||||
self.assertEqual(self.daemon.order, ["sheet.place", "sheet.frame", "laser.focus"])
|
||||
for k, v in FOUND.items():
|
||||
self.assertEqual(self.fake.state["settings"][k], v, k)
|
||||
finally:
|
||||
START_WRITES["laser.focus"] = saved
|
||||
|
||||
def test_a_program_z_beyond_the_reach_fails_before_the_burn(self):
|
||||
# A served program that commands a Z the lens does not reach is
|
||||
# refused before the card starts: nothing burns.
|
||||
from forgetest.runner import Failed
|
||||
self.daemon.stray_z = 10.6
|
||||
t = catalog.load_suite()["setup.sheet"]
|
||||
run = Run("test", t.id, t.title)
|
||||
run.unattended = True
|
||||
with self.assertRaises(Failed) as cm:
|
||||
t.fn(Context(run, None, t))
|
||||
self.assertIn("outside the lens window", str(cm.exception))
|
||||
self.assertIn("10.6", str(cm.exception))
|
||||
self.assertEqual(self.daemon.order, ["sheet.place"])
|
||||
for k, v in FOUND.items():
|
||||
self.assertEqual(self.fake.state["settings"][k], v, k)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -19,7 +19,7 @@ from forgetest import baseline, catalog
|
||||
from forgetest.runner import Context, Failed, Run
|
||||
from forgetest.suite import setup
|
||||
|
||||
IDS = ("setup.gate-blocks-controllers", "setup.override-until-reboot",
|
||||
IDS = ("setup.gate-blocks-controllers",
|
||||
"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",
|
||||
@@ -42,6 +42,8 @@ class RegistrationTests(unittest.TestCase):
|
||||
self.assertEqual(kinds[tid], "auto", tid)
|
||||
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
|
||||
self.assertNotIn("setup.override-until-reboot", self.reg) # the gate has nothing to lift
|
||||
self.assertNotIn("setup.sheet", self.reg) # the two burns, each on its own scrap
|
||||
|
||||
def test_the_record_swapping_tests_are_takeovers(self):
|
||||
for tid in ("setup.gate-blocks-controllers", "setup.advisories-rehash",
|
||||
@@ -61,13 +63,36 @@ class RegistrationTests(unittest.TestCase):
|
||||
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(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"))
|
||||
w = {"recommended": [{"id": "laser.floor", "reason": "the tube was replaced"},
|
||||
{"id": "laser.focus", "reason": "the tray was replaced"},
|
||||
{"id": "airflow", "reason": "not run yet"}, "stray"]}
|
||||
self.assertEqual(setup.flag_of(w, "laser.floor"), "the tube was replaced")
|
||||
self.assertEqual(setup.flag_of(w, "laser.focus"), "the tray was replaced")
|
||||
self.assertEqual(setup.flag_of(w, "airflow"), "not run yet")
|
||||
self.assertIsNone(setup.flag_of(w, "stray")) # only an entry with an id counts
|
||||
self.assertIsNone(setup.flag_of(w, "cameras"))
|
||||
self.assertIsNone(setup.flag_of({}, "x"))
|
||||
self.assertEqual(setup.recommended(w), {"laser.floor": "the tube was replaced",
|
||||
"laser.focus": "the tray was replaced",
|
||||
"airflow": "not run yet"})
|
||||
|
||||
def test_the_gate_parts_and_the_optional_steps_are_never_recommended(self):
|
||||
self.assertFalse(set(setup.RECOMMENDED_STEPS) & set(setup.NEVER_RECOMMENDED))
|
||||
self.assertFalse(set(setup.RECOMMENDED_STEPS) & set(setup.REQUIRED_STEPS))
|
||||
for wid in ("advisories", "account", "airflow.intakes", "laser.floor", "motion.envelope"):
|
||||
self.assertIn(wid, setup.NEVER_RECOMMENDED)
|
||||
for wid in ("lens.travel", "laser.focus", "airflow", "switches"):
|
||||
self.assertIn(wid, setup.RECOMMENDED_STEPS)
|
||||
self.assertEqual(setup.FORM_WIZARDS, ("advisories", "account", "preferences", "machine", "cloud"))
|
||||
# in the order the gate's reason names them
|
||||
self.assertEqual(setup.REQUIRED_STEPS, ("preferences", "machine", "cloud"))
|
||||
|
||||
def test_the_gate_test_starts_from_an_open_gate_and_the_intake_tests_from_a_count(self):
|
||||
reg = self.reg
|
||||
self.assertIs(reg["setup.gate-blocks-controllers"].precheck, setup.gate_is_open)
|
||||
from forgetest.suite import setup_dark
|
||||
for tid in ("setup.check-intakes", "setup.check-airflow"):
|
||||
self.assertIs(reg[tid].precheck, setup_dark.intakes_counted, tid)
|
||||
|
||||
def test_the_first_run_flow_runs_unattended_with_the_fixture_and_the_page_walk_does_not(self):
|
||||
flow = self.reg["setup.first-run-flow"]
|
||||
@@ -89,14 +114,14 @@ class RegistrationTests(unittest.TestCase):
|
||||
for want in (("forgectrl", "src/ui/wizard.*"), ("forgectrl", "src/ui/md.js"), ("forgectrl", "src/tls.*")):
|
||||
self.assertIn(want, page, want)
|
||||
|
||||
def test_the_only_attended_tests_need_a_workstation_or_the_sheet(self):
|
||||
# With the bench actuator up, three setup tests keep a
|
||||
# person: the page walk, the print from the Glowforge app, and
|
||||
# the sheet. Everything else runs from the queue.
|
||||
def test_the_only_attended_tests_need_a_workstation_or_a_scrap(self):
|
||||
# With the bench actuator up, four setup tests keep a person: the
|
||||
# page walk, the print from the Glowforge app, and the two burns,
|
||||
# each on a fresh scrap. Everything else runs from the queue.
|
||||
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, ["setup.cloud-header-capture", "setup.first-run-page",
|
||||
"setup.sheet"])
|
||||
"setup.floor", "setup.focus"])
|
||||
|
||||
def test_the_factory_return_never_runs_the_return(self):
|
||||
import inspect
|
||||
@@ -152,13 +177,43 @@ class RecordTests(unittest.TestCase):
|
||||
|
||||
def test_without_wizards_keeps_consent_and_account(self):
|
||||
rec = setup.complete_record(self.STATUS)
|
||||
rec["flags"] = {"machine": {"level": "required", "reason": "x"}}
|
||||
rec["flags"] = {"machine": {"reason": "x"}}
|
||||
out = setup.record_without_wizards(rec)
|
||||
self.assertEqual(out["wizards"], {})
|
||||
self.assertEqual(out["flags"], {})
|
||||
self.assertEqual(out["advisories"], rec["advisories"])
|
||||
self.assertEqual(out["account"], rec["account"])
|
||||
self.assertEqual(rec["flags"], {"machine": {"level": "required", "reason": "x"}}) # a copy
|
||||
self.assertEqual(rec["flags"], {"machine": {"reason": "x"}}) # a copy
|
||||
|
||||
def test_with_required_steps_keeps_them_and_nothing_else(self):
|
||||
rec = setup.complete_record(self.STATUS)
|
||||
rec["wizards"]["machine"]["result"] = {"model": "basic"}
|
||||
rec["wizards"]["switches"] = {"version": 1}
|
||||
rec["flags"] = {"machine": {"reason": "the head was replaced"}}
|
||||
out = setup.record_with_required_steps(rec)
|
||||
self.assertEqual(sorted(out["wizards"]), ["cloud", "machine", "preferences"])
|
||||
self.assertEqual(out["wizards"]["machine"]["result"], {"model": "basic"}) # kept from the record
|
||||
self.assertEqual(out["wizards"]["preferences"]["version"], 1) # written at version 1
|
||||
self.assertEqual(out["flags"], {})
|
||||
self.assertEqual(out["account"], rec["account"])
|
||||
self.assertIn("switches", rec["wizards"]) # a copy
|
||||
|
||||
def test_without_consent_keeps_the_account_and_the_wizards(self):
|
||||
rec = setup.complete_record(self.STATUS)
|
||||
out = setup.record_without_consent(rec)
|
||||
self.assertEqual(out["advisories"], {})
|
||||
self.assertNotIn("acceptance", out)
|
||||
self.assertEqual(out["account"], rec["account"])
|
||||
self.assertEqual(out["wizards"], rec["wizards"])
|
||||
self.assertIn("pressed_at", rec["acceptance"]) # a copy
|
||||
|
||||
def test_without_account_keeps_the_consent(self):
|
||||
rec = setup.complete_record(self.STATUS)
|
||||
out = setup.record_without_account(rec)
|
||||
self.assertNotIn("account", out)
|
||||
self.assertEqual(out["advisories"], rec["advisories"])
|
||||
self.assertEqual(out["acceptance"], rec["acceptance"])
|
||||
self.assertEqual(rec["account"]["name"], "bench") # a copy
|
||||
|
||||
def test_write_file_and_remove(self):
|
||||
tmp = tempfile.mkdtemp(prefix="forgetest-comm-")
|
||||
@@ -180,7 +235,6 @@ class RecordTests(unittest.TestCase):
|
||||
try:
|
||||
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)
|
||||
@@ -266,7 +320,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": "setup required: machine"}
|
||||
"gated": True, "why": "the intake fans are not counted"}
|
||||
lines = []
|
||||
body = baseline.Baseline(lines.append).wait_settled(timeout=8)
|
||||
self.assertEqual(body["controller"], "gated")
|
||||
@@ -275,14 +329,14 @@ class GatedSettleTests(unittest.TestCase):
|
||||
|
||||
class FirstRunSeedTests(unittest.TestCase):
|
||||
"""The record the first-run tests install: a first run behind the
|
||||
checks and the sheet."""
|
||||
checks and the burns."""
|
||||
|
||||
STATUS = {"wizards": [{"id": "advisories", "version": 1, "class": "form"},
|
||||
{"id": "account", "version": 1, "class": "form"},
|
||||
{"id": "cloud", "version": 1, "class": "form"},
|
||||
{"id": "switches", "version": 1, "class": "dark"},
|
||||
{"id": "motion", "version": 2, "class": "dark"},
|
||||
{"id": "sheet.frame", "version": 1, "class": "live"}]}
|
||||
{"id": "laser.focus", "version": 1, "class": "live"}]}
|
||||
|
||||
def test_no_consent_no_account_not_complete_and_every_check_done(self):
|
||||
base = {"created": "2026-09-01T00:00:00Z", "completed": "2026-09-02T00:00:00Z",
|
||||
@@ -296,14 +350,14 @@ class FirstRunSeedTests(unittest.TestCase):
|
||||
self.assertEqual(rec["advisories"], {})
|
||||
self.assertEqual(rec["created"], "2026-09-01T00:00:00Z")
|
||||
self.assertEqual(rec["flags"], {"flow_thin": True})
|
||||
self.assertEqual(sorted(rec["wizards"]), ["motion", "sheet.frame", "switches"])
|
||||
self.assertEqual(sorted(rec["wizards"]), ["laser.focus", "motion", "switches"])
|
||||
self.assertEqual(rec["wizards"]["switches"]["result"], {"lid": True}) # kept from the base
|
||||
self.assertEqual(rec["wizards"]["motion"]["version"], 2) # written at the catalog version
|
||||
|
||||
def test_without_a_base_record(self):
|
||||
rec = setup.first_run_record(self.STATUS, None)
|
||||
self.assertEqual(sorted(rec["wizards"]), ["motion", "sheet.frame", "switches"])
|
||||
self.assertEqual(rec["wizards"]["sheet.frame"]["version"], 1)
|
||||
self.assertEqual(sorted(rec["wizards"]), ["laser.focus", "motion", "switches"])
|
||||
self.assertEqual(rec["wizards"]["laser.focus"]["version"], 1)
|
||||
|
||||
|
||||
class SshdPolicyTests(unittest.TestCase):
|
||||
|
||||
Reference in New Issue
Block a user