Files
forgefirm/forgetest/tests/test_takeover_client.py
ScottW514 ef2390f7d4 forgetest: a takeover hands back the cloud client it found
In cloud mode forgectrl's start at the end of a takeover starts a cloud
client, and what that client is comes only from gfcloud's one-start
markers, which the client that starts reads and takes down. The takeover
started it bare: the online client, with the service's connect-time
hunt. Seen on the bench with exthost.armed-freeze, which runs its print
under the offline service and puts the setup record back under a
takeover: the machine came back online in cloud mode, the hunt homed the
head at 14:21:01, the run's result was written at 14:21:02, and the
service sent a head move at 14:21:08, after the run had ended, where the
next test's switch to GRBL mode would have cut it off. It also broke the
harness's own rule that every cloud client start it makes comes up
without the hunt.

The takeover now reads, on entry, whether the running cloud client is
the offline service: the client itself holds the listening socket at
/run/gfcloud-offline.sock (hw.listens_on reads /proc/net/unix and the
process's own descriptors, so a socket file another process left behind
does not count). forgectrl's start is made under the no-hunt marker in
cloud mode, and under the offline marker as well when the offline
service was found. After the start the client that came up must listen
on the offline socket within 60 s; a miss is recorded on the run's
baseline capture and the post pass turns it into a leftover ("cloud
client at the takeover end", not restored: a cloud test starts the
client it needs), which fails the run. A marker no client read (the
gate, standby, a fault) is taken down, so a later start never comes up
under it. The judge never breaks the exit path.

Proof: forgetest's unit tests pass on the host (481 OK). The new
tests/test_takeover_client.py drives a real enter and exit against the
fake forgectrl, a fake /proc, and an init script that plays gfcloud's
start: the offline service comes back offline, an online client after
it fails the run, a start that never happened takes the markers down,
an online client comes back without the hunt, a stale socket file is not
the offline service, GRBL mode starts under no marker. Negative
controls: without the markers four of them fail, without the judge
three. Bench drill on the bench reference with this package from /tmp:
GRBL mode, the offline service started as enter_offline starts it (pid
4834), a takeover; the client forgectrl brought back (pid 4898) read
both markers, logged "OFFLINE service", listened on the socket, and in
the 10 s after had no web session, no hunt, and no motion. The bench was
then handed back to GRBL mode as found.

Acceptance: runner.py, baseline.py and hw.py are in no test's
fingerprint, so no result moves; the catalog test that exercises the
change is exthost.armed-freeze, whose takeover now fails the run unless
the offline service comes back.
2026-09-25 10:45:24 -04:00

201 lines
9.5 KiB
Python

# Copyright 2026 514 LLC d/b/a OpenGlow
# Written by Scott Wiederhold
# https://community.openglow.org
# SPDX-License-Identifier: MIT
"""A takeover hands back the cloud client it found. In cloud mode
forgectrl's start at the end of a takeover starts a cloud client, and what
that client is comes only from gfcloud's one-start markers. Started bare it
is the online client with the service's connect-time hunt, and the moves
the service sends after the hunt outlast the run. The start is made under
the no-hunt marker, and under the offline marker when the takeover found
the offline service, which it then proves came back; a miss is a leftover
that fails the run. Runs against the fake forgectrl, a fake /proc, and a
fake init script that plays the client's start."""
import os
import shutil
import tempfile
import unittest
from forgetest import baseline, hw, runner
from helpers import FakeForgectrl
class FakeRun:
def __init__(self, cap):
self.baseline_captured = cap
self.lines = []
def log(self, s):
self.lines.append(s)
class TakeoverClientTests(unittest.TestCase):
OLD_PID, NEW_PID = 200, 201
def setUp(self):
self.tmp = tempfile.mkdtemp(prefix="forgetest-tkc-")
self.proc = os.path.join(self.tmp, "proc")
os.makedirs(os.path.join(self.proc, "net"))
self.sysfs = os.path.join(self.tmp, "sysfs") + os.sep
for group in ("cnc", "pic", "head"):
os.makedirs(self.sysfs + group)
os.environ["GF_SYSFS_ROOT"] = self.sysfs
os.environ["FORGETEST_PROC_ROOT"] = self.proc
os.environ["FORGETEST_MARKER"] = os.path.join(self.tmp, "forgetest.active")
self.saved = (baseline.NOHUNT_MARKER, baseline.OFFLINE_MARKER, baseline.OFFLINE_SOCKET,
hw.initd, runner.Takeover.OFFLINE_BACK_S)
baseline.NOHUNT_MARKER = os.path.join(self.tmp, "gfcloud-nohunt")
baseline.OFFLINE_MARKER = os.path.join(self.tmp, "gfcloud-offline")
baseline.OFFLINE_SOCKET = "/run/gfcloud-offline.sock"
runner.Takeover.OFFLINE_BACK_S = 0
baseline.Baseline._unreachable_until = 0.0
self.unix = [] # (pid, inode, path, listening)
self.fc = FakeForgectrl().start()
self.fc.state["mode"] = {"mode": "cloud", "controller": "running", "pid": self.OLD_PID,
"motion": "verified"}
self.calls = []
self.at_start = None # the markers present when forgectrl started
self.comes_up = "running" # the controller forgectrl's start settles on
hw.initd = self._initd
def tearDown(self):
(baseline.NOHUNT_MARKER, baseline.OFFLINE_MARKER, baseline.OFFLINE_SOCKET,
hw.initd, runner.Takeover.OFFLINE_BACK_S) = self.saved
self.fc.stop()
for k in ("GF_SYSFS_ROOT", "FORGETEST_PROC_ROOT", "FORGETEST_MARKER"):
os.environ.pop(k, None)
shutil.rmtree(self.tmp, ignore_errors=True)
# -- the fakes ------------------------------------------------------
def _socket(self, pid, inode, path, listening=True):
self.unix.append((pid, inode, path, listening))
fd_dir = os.path.join(self.proc, str(pid), "fd")
os.makedirs(fd_dir, exist_ok=True)
os.symlink("socket:[%d]" % inode, os.path.join(fd_dir, str(len(os.listdir(fd_dir)) + 3)))
with open(os.path.join(self.proc, "net", "unix"), "w") as f:
f.write("Num RefCount Protocol Flags Type St Inode Path\n")
for _pid, ino, p, lis in self.unix:
f.write("00000000: 00000002 00000000 %08X 0001 %s %d %s\n"
% (0x10000 if lis else 0, "01" if lis else "03", ino, p))
def _offline_client(self, pid):
self._socket(pid, 9000 + pid, baseline.OFFLINE_SOCKET)
def _initd(self, service, action, timeout=60):
"""forgectrl stop/start; the start plays gfcloud's: it reads and
takes down the markers, and it is the offline service when the
offline marker was there."""
self.calls.append((service, action))
if action != "start":
return 0, ""
self.at_start = sorted(os.path.basename(p) for p in (baseline.NOHUNT_MARKER, baseline.OFFLINE_MARKER)
if os.path.exists(p))
mode = self.fc.state["mode"]
if self.comes_up != "running":
self.fc.state["mode"] = dict(mode, controller=self.comes_up, pid=None, motion="unverified")
return 0, ""
offline = os.path.exists(baseline.OFFLINE_MARKER)
for p in (baseline.NOHUNT_MARKER, baseline.OFFLINE_MARKER):
if os.path.exists(p):
os.remove(p)
self.fc.state["mode"] = dict(mode, controller="running", pid=self.NEW_PID, motion="verified")
if offline and mode.get("mode") == "cloud":
self._offline_client(self.NEW_PID)
return 0, ""
def takeover(self):
cap = {"position": None, "mode": self.fc.state["mode"]["mode"]}
run = FakeRun(cap)
with runner.Takeover(run.log, "test.id", run=run):
pass
return cap, run
def markers_left(self):
return [p for p in (baseline.NOHUNT_MARKER, baseline.OFFLINE_MARKER) if os.path.exists(p)]
# -- the socket judge -------------------------------------------------
def test_the_listener_is_the_process_own_descriptor(self):
self._offline_client(self.OLD_PID)
self.assertTrue(hw.listens_on(self.OLD_PID, baseline.OFFLINE_SOCKET))
# the path alone is not enough: another process's socket, a
# connected (not listening) socket, another path, no pid
self.assertFalse(hw.listens_on(self.NEW_PID, baseline.OFFLINE_SOCKET))
self._socket(self.NEW_PID, 7001, baseline.OFFLINE_SOCKET, listening=False)
self.assertFalse(hw.listens_on(self.NEW_PID, baseline.OFFLINE_SOCKET))
self.assertFalse(hw.listens_on(self.OLD_PID, "/run/forgefirm/grbl.ctl"))
self.assertFalse(hw.listens_on(None, baseline.OFFLINE_SOCKET))
# -- the takeover ------------------------------------------------------
def test_the_offline_service_comes_back_offline(self):
# exthost.armed-freeze's case: the setup record put back under a
# takeover while the offline service ran
self._offline_client(self.OLD_PID)
cap, run = self.takeover()
self.assertEqual(self.calls, [("forgectrl", "stop"), ("forgectrl", "start")])
self.assertEqual(self.at_start, ["gfcloud-nohunt", "gfcloud-offline"])
self.assertTrue(hw.listens_on(self.NEW_PID, baseline.OFFLINE_SOCKET))
self.assertNotIn("restart_clients", cap)
self.assertEqual(self.markers_left(), [])
self.assertTrue(any("back as the offline service (pid %d)" % self.NEW_PID in l for l in run.lines),
run.lines)
def test_an_online_client_after_the_offline_service_fails_the_run(self):
# the client that came up is not listening: an online client, or
# a marker the start never saw
self._offline_client(self.OLD_PID)
real = self._initd
def lost_marker(service, action, timeout=60):
if action == "start" and os.path.exists(baseline.OFFLINE_MARKER):
os.remove(baseline.OFFLINE_MARKER)
return real(service, action, timeout)
hw.initd = lost_marker
cap, run = self.takeover()
self.assertEqual(cap["restart_clients"],
[{"where": "takeover end", "expected": "the offline service",
"found": "a client that is not the offline service (pid %d)" % self.NEW_PID}])
left = baseline.Baseline(run.log)
items = []
left._preserved(items, cap)
self.assertEqual([x.item for x in items], ["cloud client at the takeover end"])
self.assertTrue(items[0].action.startswith("not restored"), items[0].action)
def test_no_client_started_takes_the_markers_down(self):
# the gate closed, or standby: nothing read the markers, and a
# later start must not come up offline or without its hunt
self._offline_client(self.OLD_PID)
self.comes_up = "gated"
cap, run = self.takeover()
self.assertEqual(self.at_start, ["gfcloud-nohunt", "gfcloud-offline"])
self.assertEqual(self.markers_left(), [])
self.assertEqual(len(cap["restart_clients"]), 1)
self.assertIn("no cloud client running", cap["restart_clients"][0]["found"])
def test_an_online_client_comes_back_without_the_hunt(self):
cap, run = self.takeover()
self.assertEqual(self.at_start, ["gfcloud-nohunt"])
self.assertFalse(hw.listens_on(self.NEW_PID, baseline.OFFLINE_SOCKET))
self.assertNotIn("restart_clients", cap)
self.assertEqual(self.markers_left(), [])
def test_a_stale_offline_socket_file_is_not_the_offline_service(self):
# a socket another process left at the path: the online client
# found stays online
self._socket(99, 7002, baseline.OFFLINE_SOCKET)
cap, run = self.takeover()
self.assertEqual(self.at_start, ["gfcloud-nohunt"])
self.assertNotIn("restart_clients", cap)
def test_grbl_mode_starts_under_no_marker(self):
self.fc.state["mode"] = {"mode": "grbl", "controller": "running", "pid": self.OLD_PID,
"motion": "verified"}
cap, run = self.takeover()
self.assertEqual(self.at_start, [])
self.assertNotIn("restart_clients", cap)
if __name__ == "__main__":
unittest.main()