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.
201 lines
7.2 KiB
Python
201 lines
7.2 KiB
Python
# Copyright 2026 514 LLC d/b/a OpenGlow
|
|
# Written by Scott Wiederhold
|
|
# https://community.openglow.org
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""The controller mode a test declares: the runner puts the machine there
|
|
before the test and the baseline keeps it there afterward.
|
|
|
|
The cloud tests enter cloud mode and stay (the operator rule: no switch
|
|
back after every test), so a queue that runs on past them reaches the
|
|
motion tests with gfcloud as the controller and no grblHAL process to
|
|
find. A test that names its mode gets the switch made for it, once, in the
|
|
pre pass - after the leftovers are handled and before the preserved state
|
|
is captured, so the post pass hands back the mode the test asked for
|
|
rather than the one the run found.
|
|
"""
|
|
import os
|
|
import shutil
|
|
import socket
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import unittest
|
|
|
|
import helpers
|
|
from forgetest import baseline
|
|
from forgetest.log import Log
|
|
from forgetest.runner import Runner
|
|
|
|
|
|
class GrblPort:
|
|
"""A listening socket standing in for the Grbl port of grblHAL."""
|
|
|
|
def __init__(self):
|
|
self.sock = socket.socket()
|
|
self.sock.bind(("127.0.0.1", 0))
|
|
self.sock.listen(5)
|
|
os.environ["GRBL_HOST"] = "127.0.0.1"
|
|
os.environ["GRBL_PORT"] = str(self.sock.getsockname()[1])
|
|
self._stop = False
|
|
self._th = threading.Thread(target=self._accept, daemon=True)
|
|
self._th.start()
|
|
|
|
def _accept(self):
|
|
self.sock.settimeout(0.2)
|
|
while not self._stop:
|
|
try:
|
|
c, _ = self.sock.accept()
|
|
c.close()
|
|
except OSError:
|
|
pass
|
|
|
|
def close(self):
|
|
self._stop = True
|
|
self.sock.close()
|
|
for k in ("GRBL_HOST", "GRBL_PORT"):
|
|
os.environ.pop(k, None)
|
|
|
|
|
|
def cloud(fc):
|
|
fc.state["mode"] = {"mode": "cloud", "controller": "running", "pid": 7, "motion": "verified"}
|
|
fc.state["settings"]["controller_mode"] = "cloud"
|
|
|
|
|
|
class SwitchModeTests(unittest.TestCase):
|
|
"""Baseline.switch_mode against the fake forgectrl."""
|
|
|
|
def setUp(self):
|
|
self.fc = helpers.FakeForgectrl().start()
|
|
self.port = GrblPort()
|
|
baseline.Baseline._unreachable_until = 0.0
|
|
self.lines = []
|
|
|
|
def tearDown(self):
|
|
self.port.close()
|
|
self.fc.stop()
|
|
|
|
def bl(self):
|
|
return baseline.Baseline(self.lines.append)
|
|
|
|
def test_already_in_the_mode_posts_nothing(self):
|
|
ok, detail = self.bl().switch_mode("grbl")
|
|
self.assertTrue(ok, detail)
|
|
self.assertEqual(self.fc.posts, [])
|
|
|
|
def test_cloud_to_grbl_switches_and_waits_for_the_port(self):
|
|
cloud(self.fc)
|
|
b = self.bl()
|
|
ok, detail = b.switch_mode("grbl")
|
|
self.assertTrue(ok, detail)
|
|
self.assertEqual(self.fc.posts, [("/mode", {"controller": "grbl"})])
|
|
self.assertEqual(self.fc.state["mode"]["mode"], "grbl")
|
|
self.assertEqual(b.mode, "grbl")
|
|
|
|
def test_a_controller_that_never_comes_up_is_reported(self):
|
|
cloud(self.fc)
|
|
self.fc.on_post = lambda path, form: (200, {"ok": True}) # accepted, nothing happens
|
|
ok, detail = self.bl().switch_mode("grbl", timeout=2)
|
|
self.assertFalse(ok)
|
|
self.assertIn("did not come up", detail)
|
|
|
|
def test_a_refused_switch_is_reported(self):
|
|
cloud(self.fc)
|
|
self.fc.on_post = lambda path, form: (409, {"error": "busy"})
|
|
ok, detail = self.bl().switch_mode("grbl")
|
|
self.assertFalse(ok)
|
|
self.assertIn("409", detail)
|
|
|
|
def test_grbl_needs_the_port_open(self):
|
|
cloud(self.fc)
|
|
self.port.close() # grblHAL running, port never listening
|
|
os.environ["GRBL_HOST"], os.environ["GRBL_PORT"] = "127.0.0.1", "1"
|
|
old = baseline.GRBL_PORT_S
|
|
baseline.GRBL_PORT_S = 2
|
|
try:
|
|
ok, detail = self.bl().switch_mode("grbl")
|
|
finally:
|
|
baseline.GRBL_PORT_S = old
|
|
self.assertFalse(ok)
|
|
self.assertIn("Grbl port", detail)
|
|
|
|
|
|
class RunnerModeTests(unittest.TestCase):
|
|
"""The runner end to end: a declared mode is established before the
|
|
test function runs and kept by the post pass."""
|
|
|
|
def setUp(self):
|
|
self.tmp = tempfile.mkdtemp(prefix="forgetest-mode-")
|
|
os.environ["FORGETEST_DATA"] = self.tmp
|
|
os.environ["FORGETEST_MARKER"] = os.path.join(self.tmp, "marker")
|
|
self.fc = helpers.FakeForgectrl().start()
|
|
self.port = GrblPort()
|
|
baseline.Baseline._unreachable_until = 0.0
|
|
self.seen = {}
|
|
fc, seen = self.fc, self.seen
|
|
|
|
def t_sees_mode(ctx):
|
|
seen["mode"] = fc.state["mode"]["mode"]
|
|
|
|
reg = helpers.registry(
|
|
helpers.make_test("m.grbl", [("forgectrl", "src/ui.c")], fn=t_sees_mode, mode="grbl"),
|
|
helpers.make_test("m.any", [("forgectrl", "src/ui.c")], fn=t_sees_mode),
|
|
)
|
|
self.runner = Runner(Log(os.path.join(self.tmp, "results.jsonl")), helpers.make_manifest(), reg)
|
|
|
|
def tearDown(self):
|
|
self.port.close()
|
|
self.fc.stop()
|
|
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
for k in ("FORGETEST_DATA", "FORGETEST_MARKER"):
|
|
os.environ.pop(k, None)
|
|
|
|
def run_test(self, tid):
|
|
ok, msg = self.runner.start_test(tid)
|
|
self.assertTrue(ok, msg)
|
|
run = self.runner.current
|
|
deadline = time.time() + 30
|
|
while not run.finished and time.time() < deadline:
|
|
time.sleep(0.05)
|
|
self.assertTrue(run.finished, "run did not finish")
|
|
return run
|
|
|
|
def test_grbl_test_found_in_cloud_mode_gets_the_switch_and_keeps_it(self):
|
|
cloud(self.fc)
|
|
run = self.run_test("m.grbl")
|
|
self.assertEqual(run.finished["result"], "PASS", run.finished)
|
|
self.assertEqual(self.seen["mode"], "grbl")
|
|
# one switch, made before the test; the post pass keeps grbl
|
|
self.assertEqual(self.fc.posts, [("/mode", {"controller": "grbl"})])
|
|
self.assertEqual(self.fc.state["mode"]["mode"], "grbl")
|
|
self.assertEqual(run.baseline_captured["mode"], "grbl")
|
|
self.assertEqual(run.evidence["mode"]["found"], "cloud")
|
|
self.assertTrue(run.evidence["mode"]["switched"])
|
|
self.assertTrue(any("mode: test needs grbl" in ln for ln in run.lines), run.lines)
|
|
|
|
def test_grbl_test_in_grbl_mode_switches_nothing(self):
|
|
run = self.run_test("m.grbl")
|
|
self.assertEqual(run.finished["result"], "PASS", run.finished)
|
|
self.assertEqual(self.fc.posts, [])
|
|
self.assertFalse(run.evidence["mode"]["switched"])
|
|
|
|
def test_undeclared_test_runs_in_the_mode_it_finds(self):
|
|
cloud(self.fc)
|
|
run = self.run_test("m.any")
|
|
self.assertEqual(run.finished["result"], "PASS", run.finished)
|
|
self.assertEqual(self.seen["mode"], "cloud")
|
|
self.assertEqual(self.fc.posts, [])
|
|
self.assertNotIn("mode", run.evidence)
|
|
|
|
def test_a_switch_that_fails_fails_the_test_before_it_runs(self):
|
|
cloud(self.fc)
|
|
self.fc.on_post = lambda path, form: (503, {"error": "supervisor busy"})
|
|
run = self.run_test("m.grbl")
|
|
self.assertEqual(run.finished["result"], "FAIL")
|
|
self.assertIn("needs grbl mode", run.finished["message"])
|
|
self.assertNotIn("mode", self.seen)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|