mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-10-15 17:19:55 -07:00
Each left after its host cases passed (forgectrl 1dc85f6) and with the coverage lint clean at the pins, its only failure the known setup.check-lens entry on src/lensfind.*: - forgectrl.tokens: host_routes.sh's scoped tokens (a route refused to a token not given it, neither token in the list or the store), with tokens_test and auth_scoped_test. - update.product-gate: host_update.sh's product gate on the upload, with fwproduct_test. - setup.first-run-page: tests/wizard_walk.py. - setup.check-sensors, setup.check-intakes, setup.check-cameras, setup.check-flow-verify, setup.check-envelope, setup.floor: host_setup.sh's checks and the floor burn, with wizcalc_test. envelope_test.py's cases are unchanged. setup.floor's emission stays with setup.focus and laser.emission-witness. - motion.job: jobpost_test, with jobrun_test and lease_test; jobpost.* is covered by exthost.motion-job, which stays. - exthost.motion-jog: host_routes.sh's credential case. Two HOST_TESTED rows, each a binary forgectrl's build.yml runs at the pin c274f19 that proves the path the lint named: src/fwproduct.* to fwproduct_test, src/grblport.* to grblport_test. exthost.motion-job now requires motion.jog-roundtrip in place of motion.job. bedsize.py held setup.check-envelope alone and goes; forgectrl.py loses _words_of and its hw import, motion.py the job runner's helpers, setup_burns.py FLOOR_KEY and FLOOR_KEYS. forgetest's own tests: test_setup_dark.py loses the sensors and intakes judges and the departed ids; test_setup_burns.py loses the floor's cases, and the guards burn() and preview_ok() hold for the focus burn (the program's tail and reach, the thickness in inches and the one refused, a prompt not answered, the witnesses) run on setup.focus. Stay: forgectrl.auth (the fuse view's button refusal on the real button); forgectrl.settings-bounds (the lid lamp: cam.c writes /sys/glowforge/pic/lid_led, not under GF_SYSFS_ROOT); forgectrl.panel-serves (src/ui.h, which no binary at the pin proves; Z inside the lens reach is setup.check-lens's); logs.routing; setup.account-login (the account's render and its home, image content); setup.ssh-until-reboot (src/paths.h); setup.machine-name (udhcpc -x hostname and no mDNS responder, image content); setup.first-run-flow (src/button.*, led.*, advisories.*, hooks.h and the four advisory documents); exthost.motion-job (jobpost.*, which no binary at the pin proves). Proof: the coverage lint, 55 tests; pyflakes; test_setup_burns, test_setup_dark, test_takeover_position and test_image (54 OK). Shared text moved the fingerprints of exthost.armed-freeze, exthost.platform, exthost.service, forgectrl.auth, forgectrl.panel-serves, forgectrl.settings-bounds, laser.armed-kill, laser.emission-witness, laser.pause-resume-lid-cancel, motion.cancel-abort, motion.deadman, motion.jog-roundtrip, motion.liveness-probe, motion.microstep-modes, motion.pacing, motion.release, motion.step-timing-under-load, setup.account-login, setup.check-airflow, setup.check-lens, setup.check-motion, setup.check-switches, setup.cloud-header-capture, setup.first-run-flow, setup.focus, setup.machine-name, setup.ssh-until-reboot and update.slots-and-signature; exthost.motion-job's own text moved with its requires.
191 lines
7.9 KiB
Python
191 lines
7.9 KiB
Python
# Copyright 2026 514 LLC d/b/a OpenGlow
|
|
# Written by Scott Wiederhold
|
|
# https://community.openglow.org
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""A takeover judges the head where the baseline cannot. forgectrl's start
|
|
at the end of a takeover is a controller start, which re-zeroes the step
|
|
counters wherever the head stands, so after it a head left out reads as
|
|
home. The takeover reads the counters as the controller goes away (against
|
|
where the run expects the head) and before it comes back (against where the
|
|
takeover found it); a miss is a leftover that fails the run, and the head is
|
|
never moved on it. Runs against a fake sysfs tree and a fake anchor file."""
|
|
import os
|
|
import shutil
|
|
import struct
|
|
import tempfile
|
|
import unittest
|
|
|
|
from forgetest import baseline, hw, runner
|
|
|
|
|
|
class FakeRun:
|
|
def __init__(self, cap):
|
|
self.baseline_captured = cap
|
|
self.lines = []
|
|
|
|
def log(self, s):
|
|
self.lines.append(s)
|
|
|
|
|
|
class TakeoverPositionTests(unittest.TestCase):
|
|
def setUp(self):
|
|
# the clean machine test_baseline.py builds, so the post pass
|
|
# finds nothing but what a test here leaves
|
|
self.tmp = tempfile.mkdtemp(prefix="forgetest-tko-")
|
|
self.sysfs = os.path.join(self.tmp, "sysfs") + os.sep
|
|
self.leds = os.path.join(self.tmp, "leds") + os.sep
|
|
for group in ("cnc", "pic", "head", "thermal"):
|
|
os.makedirs(self.sysfs + group)
|
|
for name in baseline.BUTTON_LEDS + ("lid_led",):
|
|
os.makedirs(self.leds + name)
|
|
with open(self.leds + name + "/brightness", "w") as f:
|
|
f.write("0")
|
|
for attr, val in baseline.fixed_sysfs() + baseline.IDLE_READBACKS:
|
|
self._attr(attr, val)
|
|
self._attr("cnc/interlock_circuit", "45")
|
|
self._attr("pic/lid_led", "0")
|
|
self._pos(0, 0, 0)
|
|
os.environ["GF_SYSFS_ROOT"] = self.sysfs
|
|
os.environ["GF_LEDS_ROOT"] = self.leds
|
|
os.environ["FORGECTRL_URL"] = "http://127.0.0.1:1" # nothing listens
|
|
baseline.Baseline._unreachable_until = 0.0
|
|
self.anchor = os.path.join(self.tmp, "grblhal.homed")
|
|
self._new_anchor(1)
|
|
self.old_anchor, baseline.ANCHOR_PATH = baseline.ANCHOR_PATH, self.anchor
|
|
self.spm = baseline.counter_steps_per_mm()
|
|
|
|
def tearDown(self):
|
|
baseline.ANCHOR_PATH = self.old_anchor
|
|
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
for k in ("GF_SYSFS_ROOT", "GF_LEDS_ROOT", "FORGECTRL_URL"):
|
|
os.environ.pop(k, None)
|
|
|
|
def _attr(self, attr, val):
|
|
with open(self.sysfs + attr, "w") as f:
|
|
f.write(str(val))
|
|
|
|
def _pos(self, x, y, z):
|
|
with open(self.sysfs + "cnc/position", "wb") as f:
|
|
f.write(struct.pack("<5i", x, y, z, 0, 0))
|
|
|
|
def _new_anchor(self, ns):
|
|
"""A controller start or a home: a new anchor file."""
|
|
with open(self.anchor + ".new", "w") as f:
|
|
f.write("0 0 0 4 startup")
|
|
os.replace(self.anchor + ".new", self.anchor)
|
|
os.utime(self.anchor, ns=(ns, ns))
|
|
|
|
def cap(self):
|
|
return {"position": baseline.read_position(), "frame": baseline.counter_frame()}
|
|
|
|
def takeover(self, cap):
|
|
run = FakeRun(cap)
|
|
return runner.Takeover(run.log, "test.id", run=run), run
|
|
|
|
def mm(self, mm):
|
|
return round(mm * self.spm)
|
|
|
|
def start(self, t):
|
|
"""Takeover.__enter__'s order: the frame is read while the
|
|
controller runs, then its exit takes the anchor with it (forgectrl
|
|
unlinks it) and leaves the counters, which are then read."""
|
|
t.frame = baseline.counter_frame()
|
|
if os.path.exists(self.anchor):
|
|
os.unlink(self.anchor)
|
|
t.position_at_start()
|
|
|
|
def test_a_head_where_the_run_expects_it_is_not_recorded(self):
|
|
cap = self.cap()
|
|
t, run = self.takeover(cap)
|
|
self.start(t)
|
|
t.position_at_end()
|
|
self.assertNotIn("restart_positions", cap)
|
|
|
|
def test_a_head_left_out_at_the_start_is_recorded(self):
|
|
cap = self.cap()
|
|
self._pos(self.mm(10), self.mm(10), 0)
|
|
t, run = self.takeover(cap)
|
|
self.start(t)
|
|
self.assertEqual(cap["restart_positions"],
|
|
[{"where": "takeover start", "found": [self.mm(10), self.mm(10), 0],
|
|
"expected": [0, 0, 0]}])
|
|
|
|
def test_the_frame_is_read_before_the_controller_goes(self):
|
|
# found on the bench: forgectrl unlinks the anchor when the
|
|
# controller exits, so a frame read after the stop never matched
|
|
# and the check judged nothing. Read before the stop, it does.
|
|
cap = self.cap()
|
|
self._pos(self.mm(10), self.mm(10), 0)
|
|
late, run = self.takeover(cap)
|
|
os.unlink(self.anchor)
|
|
late.frame = baseline.counter_frame()
|
|
late.position_at_start()
|
|
self.assertNotIn("restart_positions", cap)
|
|
self._new_anchor(1)
|
|
cap = self.cap() # a run that began at the origin in this frame
|
|
cap["position"] = [0, 0, 0]
|
|
on_time, run = self.takeover(cap)
|
|
self.start(on_time)
|
|
self.assertEqual([r["where"] for r in cap["restart_positions"]], ["takeover start"])
|
|
|
|
def test_the_dead_band_is_not_a_leftover(self):
|
|
cap = self.cap()
|
|
self._pos(self.mm(0.05), 0, 0)
|
|
t, run = self.takeover(cap)
|
|
self.start(t)
|
|
self.assertNotIn("restart_positions", cap)
|
|
|
|
def test_the_post_pass_fails_the_run_and_moves_nothing(self):
|
|
# a check's case: a declared camera home, the head
|
|
# left out, the record put back under a takeover, whose restart
|
|
# zeroes the counters where the head stands
|
|
cap = self.cap()
|
|
self._new_anchor(3) # the camera home
|
|
cap["position"], cap["rezero_declared"], cap["frame"] = [0, 0, 0], True, baseline.counter_frame()
|
|
self._pos(self.mm(10), self.mm(10), 0)
|
|
t, run = self.takeover(cap)
|
|
self.start(t)
|
|
t.position_at_end()
|
|
self._new_anchor(4) # forgectrl's start: a controller start
|
|
self._pos(0, 0, 0)
|
|
lines = []
|
|
left = baseline.Baseline(lines.append).enforce("post", captured=cap)
|
|
self.assertEqual([x.item for x in left], ["position at the takeover start"])
|
|
self.assertTrue(left[0].action.startswith("unrestorable"), left[0].action)
|
|
self.assertIn("not moved", left[0].action)
|
|
self.assertEqual(baseline.read_position(), [0, 0, 0])
|
|
# without the takeover's record the same run reads clean: the gap
|
|
del cap["restart_positions"]
|
|
self.assertEqual(baseline.Baseline(lines.append).enforce("post", captured=cap), [])
|
|
|
|
def test_a_failed_reading_never_keeps_forgectrl_down(self):
|
|
# the exit path restores the attributes, relocks the latch and
|
|
# starts forgectrl whatever the position reading does
|
|
os.environ["FORGETEST_MARKER"] = os.path.join(self.tmp, "marker")
|
|
calls = []
|
|
real = (hw.initd, baseline.read_position, runner.Takeover.wait_settled)
|
|
hw.initd = lambda service, action, timeout=60: (calls.append((service, action)) or (0, ""))
|
|
|
|
def broken():
|
|
raise OSError("unreadable")
|
|
baseline.read_position = broken
|
|
runner.Takeover.wait_settled = lambda self: None
|
|
try:
|
|
cap = {"position": [0, 0, 0], "frame": baseline.counter_frame()}
|
|
t, run = self.takeover(cap)
|
|
t.found = ([0, 0, 0], hw.sysfs_read("cnc/x_mode"))
|
|
t.saved = {"cnc/x_mode": hw.sysfs_read("cnc/x_mode")}
|
|
t.__exit__(None, None, None)
|
|
finally:
|
|
hw.initd, baseline.read_position, runner.Takeover.wait_settled = real
|
|
os.environ.pop("FORGETEST_MARKER", None)
|
|
self.assertEqual(calls, [("forgectrl", "start")])
|
|
with open(self.sysfs + "cnc/laser_latch") as f:
|
|
self.assertEqual(f.read().strip(), "1")
|
|
self.assertTrue(any("could not be judged" in l for l in run.lines), run.lines)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|