mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-10-15 17:19:55 -07:00
The cases that tested a mock, a timer, a wording or a table, or that
replayed a judgment a host test already holds, leave forgetest/tests.
What catches a false PASS, keeps the bench safe, or guards the gate
stays.
- test_baseline: the fake sysfs tree is a mixin with no test_ methods,
so ExtensionFreeTests and BaselineModeTests no longer re-run
BaselineTests' 25 cases (133 runs, now 45). CoolPublishTests, the
lamp-fade cases and TransientNotLeftoverTests go; PositionDeadbandTests
keeps a reading past the band and an unreadable reading, both never
excused. Both latch-relock cases stay.
- test_fixture: MdnsTests, the three timing cases, the probe's log
wording, and nine prompt-routing variants go. A live test never runs
unattended, the arm-press opt-in, the short-press edge, the presence
press handing over the arm press, and the arm notice dropping at the
press stay (35 cases, now 21).
- test_cloud_suite: three false-PASS guards stay: the latch open at a
dark print's button, a lid stop that is not edge-driven, the kernel
refusing the resume. The guards for the latch unlocking in the hold
and a paused print resuming left with their catalog tests in 6a434bb.
The rest go, with the harness pieces and the hunt-lid fixture only
they read (41 cases, now 3).
- test_cooling_suite: GateOffTests, CriticalTierTests, FanGateTests and
FansQuietTests go with their harness; the flow verdict's log reads
stay: only the text after the offset, never an older verdict before it
(the flow-under-load false-PASS guard), a rotated log read whole.
- test_queue: RunClockTests and three ordering cases go, and refusals
are judged by a reason given, not its words; both live
acknowledgment cases stay.
- test_takeover_position: the six cases that judge the head and move
nothing stay.
- test_camera_home_return: the five cases where the head must not move
stay, and so do the three that check the hand-back jog's direction and
distance (homed, unhomed with the counters not counted twice, after
cloud mode): a wrong sign or a double count drives the head into the
gantry stops (15 cases, now 8).
- test_server: the keep-alive and ETag cases, the takeover and scope
half of the bench case, and the page's self-contained asserts
(test_page.py's) go.
- test_operator: the wording cases, the fixture case (test_fixture.py's)
and JournalTests go.
- test_mode: the supervisor timeout table goes.
- test_takeover_client: the listener, the offline service back, the
online client without the hunt, and GRBL under no marker stay.
- test_responsiveness: PageTests and ImplementationCostTests go; the
log cache keeps appends, a partial line, a replaced file and a fresh
list, and the fingerprint cache a changed manifest and one with no
content hash.
- test_bench_registry: GfbenchTests and two description and argument
pins go.
- test_installer: the log-format and TLS-wording cases and
InstallerShapeTests go; the die case checks the reason is in the log,
not its format.
- test_image: KernelIdentTests and the registration pin go.
Each file's __main__ guard now ends the file, and the docstrings say what
each still proves.
Proof: each of the fifteen files passes under Linux as forgetest-ci
discovers it, and pyflakes reports nothing new in them.
Acceptance: none; test-only.
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):
|
|
# setup.check-envelope'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()
|