setup.focus: the head's place judged against the stops its manual home declared, read from the home anchor

head_at_home placed the gantry stops at minus manual_home_x and
manual_home_y as the settings read at the burn. A manual home reads
them once: it declares the head at the stops at minus the offsets, and
with an offset it moves the head to X0 Y0, that far in front of them. A
write of the offsets after the home restarts no controller and keeps
the X/Y reference. So a home made with 5 and 3, the offsets then
cleared, had the head at X0 Y0 judged home 5 and 3 mm in front of the
stops, and the burn started: the false PASS 2172ae4 set out to close.
The other way round, offsets set after a home made with none failed a
head that was at the stops. The home anchor holds the stops as the home
declared them: /status reports the anchor's X and Y plus the step
counters.

head_at_home now takes the stops from the X and Y of the anchor
(baseline.ANCHOR_PATH) when /status reports X and Y referenced by a
manual home, and judges the head against them within HOME_TOL_MM. An
anchor that is gone, or that is not a manual home of X and Y, fails the
test before any start. The settings are read for the remedy alone. With
an offset, the remedy names how the push ends: release the motors, push
the head against the stops, and end the release with Energize motors
($ME), not with Set home here, which moves the head off the stops to X0
Y0 again. The remedy before said only to release the motors and push
the head, and a release left standing makes the burn ask home-push,
which the test does not answer. Without an offset it stays "home it
there". The judge only refuses more: no energy or motion path changes.

Proof: tests/test_setup_burns.py, the Unit tests step of forgetest-ci.yml
(python -B -m unittest discover -s tests), 24 tests OK on Windows with
python 3.14 and on Ubuntu 24.04 with python 3.12. BurnRunTests plants
the anchor of a home with no offset beside the fake's /status, and
test_a_manual_home_offset_puts_the_stops_off_x0_y0 plants the anchor of
each home: a home made with 5 and 3 fails the head at X0 Y0 and passes
it at X-5 Y-3; the offsets cleared after that home still fail the head
at X0 Y0 before any start (no wizard start, no lever); the offsets set
after a home made with none pass the head at X0 Y0; an anchor that is
gone, or that reads as the startup's Z-only reference, fails before any
start. That last pair replaces the leg where an offset that did not
read as a number failed: the stops no longer come from the settings,
and the rule that leg held (no stops placed, nothing starts) holds on
the anchor. test_setup_dark.py (21) and test_manifest.py (39, with the
guard on the suite's package imports, which setup_burns.py's import of
baseline.py passes) pass. Negative controls on scratch copies:
head_at_home as it was fails the case at the offsets cleared after the
home (Failed not raised), and so do the stops read from the settings;
an anchor that cannot be read taken as X0 Y0 fails it at the anchor
that is gone; an anchor of any axes and any source fails it at the
startup anchor. A probe of the failure text: with the offsets 5 and 3
it names Energize motors ($ME), not Set home here; with none it says
"home it there". pyflakes reports no undefined name.

Acceptance: setup.focus's fingerprint moves (setup_burns.py's shared
text and the test's own description); the other 42 do not move
(source_sha compared before and after). baseline.py is a shared judge
already, in every hash. Against the campaign this series already
needs, that adds no run: 850498f moved all 43 once.
This commit is contained in:
ScottW514
2026-10-16 23:28:00 -04:00
parent ce73dbb052
commit 36c1b75bb8
2 changed files with 102 additions and 46 deletions
+44 -9
View File
@@ -21,7 +21,7 @@ import unittest
sys.path.insert(0, os.path.dirname(__file__))
import helpers # noqa: E402
from forgetest import catalog # noqa: E402
from forgetest import baseline, catalog # noqa: E402
from forgetest.runner import Aborted, Context, Failed, Run # noqa: E402
from forgetest.suite import setup_burns, setup_dark # noqa: E402
@@ -291,14 +291,19 @@ class BurnRunTests(unittest.TestCase):
os.environ["GF_SYSFS_ROOT"] = self.sysfs + "/"
self.fake = helpers.FakeForgectrl().start()
# the head at its home against the gantry stops, X and Y referenced
# by a manual home (the fake's pos is X0 Y0 Z0)
# by a manual home with no offset (the fake's pos is X0 Y0 Z0), its
# anchor declaring the stops at X0 Y0
self.fake.state["status"].update({"homed": True, "homed_axes": 7, "home_source": "manual"})
self.saved_anchor = baseline.ANCHOR_PATH
baseline.ANCHOR_PATH = os.path.join(self.sysfs, "grblhal.homed")
self.anchor("0.000 0.000 0.000 7 manual")
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
baseline.ANCHOR_PATH = self.saved_anchor
self.fake.stop()
if self.saved_root is None:
os.environ.pop("GF_SYSFS_ROOT", None)
@@ -312,6 +317,12 @@ class BurnRunTests(unittest.TestCase):
with open(path, "w") as f:
f.write(value + "\n")
def anchor(self, line):
"""The controller's home anchor as a home writes it: X Y Z, the
axes it references, and what set it."""
with open(baseline.ANCHOR_PATH, "w") as f:
f.write(line + "\n")
def run_test(self, tid):
t = catalog.load_suite()[tid]
run = self.run = Run("test", t.id, t.title) # kept for a run that fails
@@ -451,11 +462,13 @@ class BurnRunTests(unittest.TestCase):
self.assert_found()
def test_a_manual_home_offset_puts_the_stops_off_x0_y0(self):
# manual_home_x 5 and manual_home_y 3: a manual home declares the
# stops X-5 Y-3 and leaves the head at X0 Y0, in front of them,
# where the wizard does not draw from. X0 Y0 fails before any start,
# with no controller lever touched; the head at the stops is home.
# A manual home made with manual_home_x 5 and manual_home_y 3
# declares the stops X-5 Y-3 (its anchor) and leaves the head at
# X0 Y0, in front of them, where the wizard does not draw from. X0
# Y0 fails before any start, with no controller lever touched; the
# head at the stops is home.
self.fake.state["settings"].update({"manual_home_x": "5", "manual_home_y": "3"})
self.anchor("-5.000 -3.000 0.000 7 manual")
with self.assertRaises(Failed) as cm:
self.run_test("setup.focus")
self.assertIn("not at its home", str(cm.exception))
@@ -467,12 +480,34 @@ class BurnRunTests(unittest.TestCase):
run = self.run_test("setup.focus")
self.assertTrue(run.evidence["head_at_start"]["judged"])
self.assertEqual(self.daemon.order, ["laser.focus"])
# an offset that does not read as a number places no stops: nothing starts
self.fake.state["settings"]["manual_home_x"] = "five"
# The offsets cleared after that home leave its reference as it is:
# the stops are still X-5 Y-3, and the head at X0 Y0 still fails
# before any start.
for k in ("manual_home_x", "manual_home_y"):
self.fake.state["settings"].pop(k)
self.fake.state["status"]["pos"] = {"x": 0.0, "y": 0.0, "z": 0.0}
with self.assertRaises(Failed) as cm:
self.run_test("setup.focus")
self.assertIn("manual_home_x reads 'five'", str(cm.exception))
self.assertIn("not at its home", str(cm.exception))
self.assertEqual(self.daemon.order, ["laser.focus"])
# The offsets set after a home made with none: the head at X0 Y0 is
# at the stops that home declared, and the burn runs.
self.fake.state["settings"].update({"manual_home_x": "5", "manual_home_y": "3"})
self.anchor("0.000 0.000 0.000 7 manual")
run = self.run_test("setup.focus")
self.assertEqual(run.evidence["head_at_start"]["stops"], {"x": 0.0, "y": 0.0})
self.assertEqual(self.daemon.order, ["laser.focus", "laser.focus"])
# An anchor that is gone, or that is not the manual home /status
# reports, places no stops: nothing starts.
os.unlink(baseline.ANCHOR_PATH)
with self.assertRaises(Failed) as cm:
self.run_test("setup.focus")
self.assertIn("home anchor", str(cm.exception))
self.anchor("0.000 0.000 0.000 4 startup")
with self.assertRaises(Failed) as cm:
self.run_test("setup.focus")
self.assertIn("home anchor", str(cm.exception))
self.assertEqual(self.daemon.order, ["laser.focus", "laser.focus"])
def test_an_unreferenced_head_runs_on_the_operators_word(self):
# A fresh controller's counters-only position places nothing: the