mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
forgetest: every run starts from, and leaves, the fresh-boot idle state
A baseline pass brackets every test and bench tool: before the run the machine is verified against the fresh-boot idle state and anything off it is restored; after the run - pass, fail, or abort - it is restored again. Fixed items are the resting values the boot establishes (module defaults, forgectrl's start-up writes, the GRBL controller's init writes) and forgectrl's idle picture (controller running with motion verified, no diagnostic, camera and cooling engines idle); preserved items (lid lamp level, position counters, settings map, controller mode) are captured before and handed back after. Deviations are leftovers: in the run pane, in the result's evidence, and on the page - attributed to the previous run when found before, to the run itself when found after. forgetest takes a fresh-boot reference once per boot (within ten minutes of boot, after the supervisor settles) as the session's resting lid-lamp level and the check on the fixed values; the values were confirmed against a fresh boot of the dev image on the bench (step_freq rests at 28160, the controller's default tick, not the probe's 10000). Takeover runs capture the controller-owned kernel attributes on entry and write them back before forgectrl restarts: the bench found the kernel tests leaving motor_lock=15 behind, which masked the supervisor's liveness probe - no motion by construction, a false driver-wedge verdict, the rail-off ladder, and finally motion-fault. The takeover wrapper also waits for the supervisor to settle on both sides (moved into baseline). Catalog consequence: none beyond the runner; the tests' own drills are unchanged.
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
"""The baseline: fixed resting values are restored, preserved values are
|
||||
handed back, every deviation is a recorded leftover. Runs against a fake
|
||||
sysfs tree; forgectrl is unreachable (service-side checks skip)."""
|
||||
import os
|
||||
import shutil
|
||||
import struct
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from forgetest import baseline
|
||||
|
||||
|
||||
class BaselineTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.mkdtemp(prefix="forgetest-bl-")
|
||||
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)
|
||||
self._led(name, "0")
|
||||
# a clean machine
|
||||
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.lines = []
|
||||
|
||||
def tearDown(self):
|
||||
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 _read(self, attr):
|
||||
with open(self.sysfs + attr) as f:
|
||||
return f.read().strip()
|
||||
|
||||
def _led(self, name, val):
|
||||
with open(self.leds + name + "/brightness", "w") as f:
|
||||
f.write(val)
|
||||
# the class interface writes 'target'; the fake mirrors it into brightness
|
||||
# only when the test asks (see _sync_leds)
|
||||
|
||||
def _sync_leds(self):
|
||||
for name in baseline.BUTTON_LEDS:
|
||||
p = self.leds + name + "/target"
|
||||
if os.path.exists(p):
|
||||
with open(p) as f:
|
||||
v = f.read().strip()
|
||||
with open(self.leds + name + "/brightness", "w") as f:
|
||||
f.write(v)
|
||||
|
||||
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 bl(self):
|
||||
return baseline.Baseline(self.lines.append)
|
||||
|
||||
def test_clean_machine_has_no_leftovers(self):
|
||||
left = self.bl().enforce("pre", captured=None)
|
||||
self.assertEqual(left, [])
|
||||
self.assertTrue(any("pre: clean" in l for l in self.lines))
|
||||
|
||||
def test_fixed_values_are_restored_and_recorded(self):
|
||||
self._attr("cnc/motor_lock", "15")
|
||||
self._attr("cnc/step_freq", "10000")
|
||||
self._attr("cnc/streaming", "1")
|
||||
left = self.bl().enforce("post", captured=None)
|
||||
items = {x.item: x for x in left}
|
||||
self.assertEqual(set(items), {"cnc/motor_lock", "cnc/step_freq", "cnc/streaming"})
|
||||
for x in left:
|
||||
self.assertEqual(x.action, "restored", str(x))
|
||||
self.assertEqual(self._read("cnc/motor_lock"), "8")
|
||||
self.assertEqual(self._read("cnc/step_freq"), "28160")
|
||||
self.assertEqual(self._read("cnc/streaming"), "0")
|
||||
self.assertEqual(items["cnc/motor_lock"].found, "15")
|
||||
self.assertEqual(items["cnc/motor_lock"].expected, "8")
|
||||
|
||||
def test_unlocked_latch_is_relocked(self):
|
||||
self._attr("cnc/interlock_circuit", "5") # bit 3 clear = unlocked
|
||||
left = self.bl().enforce("post", captured=None)
|
||||
self.assertEqual([x.item for x in left], ["laser_latch"])
|
||||
self.assertEqual(self._read("cnc/laser_latch"), "1")
|
||||
|
||||
def test_readonly_deviation_is_unrestorable(self):
|
||||
self._attr("cnc/state", "disabled")
|
||||
left = self.bl().enforce("post", captured=None)
|
||||
self.assertEqual([(x.item, x.action) for x in left], [("cnc/state", "unrestorable")])
|
||||
|
||||
def test_button_leds_are_turned_off(self):
|
||||
self._led("button_led_2", "255")
|
||||
left = self.bl().enforce("post", captured=None)
|
||||
self.assertEqual([x.item for x in left], ["leds/button_led_2"])
|
||||
self._sync_leds()
|
||||
self.assertEqual(baseline.read_led("button_led_2"), "0")
|
||||
|
||||
def test_preserved_lamp_and_position(self):
|
||||
b = self.bl()
|
||||
self._attr("pic/lid_led", "132")
|
||||
cap = b.capture()
|
||||
self.assertEqual(cap["sysfs"]["pic/lid_led"], "132")
|
||||
self.assertEqual(cap["position"], [0, 0, 0])
|
||||
# the run turned the lamp off and shifted the counters
|
||||
self._attr("pic/lid_led", "0")
|
||||
self._pos(1000, 0, 0)
|
||||
left = b.enforce("post", captured=cap)
|
||||
items = {x.item: x for x in left}
|
||||
self.assertEqual(set(items), {"pic/lid_led", "position"})
|
||||
self.assertEqual(items["pic/lid_led"].action, "restored")
|
||||
self.assertEqual(self._read("pic/lid_led"), "132")
|
||||
self.assertEqual(items["position"].action, "unrestorable")
|
||||
self.assertEqual(items["position"].found, [1000, 0, 0])
|
||||
|
||||
def test_session_resting_lamp_from_boot_reference(self):
|
||||
# the pre pass hands the lamp back to the boot level
|
||||
self._attr("pic/lid_led", "77")
|
||||
session = {"sysfs": {"pic/lid_led": "0"}}
|
||||
left = self.bl().enforce("pre", captured=session)
|
||||
self.assertEqual([(x.item, x.action) for x in left], [("pic/lid_led", "restored")])
|
||||
self.assertEqual(self._read("pic/lid_led"), "0")
|
||||
|
||||
def test_no_sysfs_means_skip(self):
|
||||
os.environ["GF_SYSFS_ROOT"] = os.path.join(self.tmp, "nope") + os.sep
|
||||
left = self.bl().enforce("pre", captured=None)
|
||||
self.assertEqual(left, [])
|
||||
self.assertTrue(any("kernel sysfs not present" in l for l in self.lines))
|
||||
|
||||
def test_boot_reference_needs_a_recent_boot(self):
|
||||
os.environ["FORGETEST_BOOT_ID"] = "test-boot"
|
||||
try:
|
||||
# no reference file, uptime unknown on a host without /proc/uptime,
|
||||
# or too old: None, with the reason logged
|
||||
ref = baseline.boot_reference(self.lines.append, self.tmp)
|
||||
up = baseline.uptime_s()
|
||||
if up is None or up > baseline.BOOT_MAX_AGE_S:
|
||||
self.assertIsNone(ref)
|
||||
self.assertTrue(any("no fresh-boot reference" in l for l in self.lines))
|
||||
else:
|
||||
# a young host: the reference is taken from the fake tree
|
||||
self.assertIsNotNone(ref)
|
||||
self.assertEqual(ref["sysfs"]["cnc/motor_lock"], "8")
|
||||
self.assertTrue(os.path.exists(os.path.join(self.tmp, "boot-test-boot.json")))
|
||||
# and loaded back the second time
|
||||
self.lines[:] = []
|
||||
ref2 = baseline.boot_reference(self.lines.append, self.tmp)
|
||||
self.assertEqual(ref2["ts"], ref["ts"])
|
||||
self.assertTrue(any("reference loaded" in l for l in self.lines))
|
||||
finally:
|
||||
os.environ.pop("FORGETEST_BOOT_ID", None)
|
||||
|
||||
def test_fixed_constants_checked_against_a_dump(self):
|
||||
ref = {"sysfs": {"cnc/motor_lock": "8", "cnc/step_freq": "10000"}}
|
||||
diffs = baseline.check_fixed_against(ref, self.lines.append)
|
||||
self.assertEqual(diffs, ["cnc/step_freq: boot=10000 constant=28160"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user