mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 08:41:13 -07:00
forgetest: fans-quiet also proves a daemon restart on a busy machine returns to idle airflow
A forgectrl started while the kernel is not idle takes the cooldown airflow (forgectrl's busy-start rule), and on the bench it kept it: after kernel.fire-line's takeover restarted the daemon with the kernel in the drill's safe state, the exhaust ran at 6200 rpm on an idle machine until the daemon was restarted by hand. cooling.fans-quiet-after-motion gains the case: forgectrl stopped, cnc/disable written, forgectrl started, and within 90 s the controller must be running with the idle duties applied. The host replay stubs the init script and holds both outcomes: the idle duties after the start, and a daemon that keeps the cooldown duties. Bench: with forgectrl 522cdb2, the busy start logged, idle airflow one tick later, the duties idle 15 s after the start, PASS. Catalog consequence: the cooling.* implementation hashes move.
This commit is contained in:
@@ -83,6 +83,7 @@ SAMPLE_S = 5 # tach sampling period (the big exhaust fan coasts f
|
||||
STABLE_SAMPLES = 3 # consecutive agreeing samples that make an idle reference
|
||||
IDLE_REF_TIMEOUT_S = 180 # a previous test's cooldown + spin-down
|
||||
COOLDOWN_TIMEOUT_S = 240 # the engine's smoke phase + idle drop + spin-down
|
||||
RESTART_IDLE_TIMEOUT_S = 90 # a daemon restart: the controller's start, its liveness probe, the engine's tick
|
||||
|
||||
|
||||
@test("cooling.flow-verify", title="Coolant flow check separates flow from no-flow",
|
||||
@@ -156,15 +157,19 @@ def _tach_stable(a, b):
|
||||
return all(abs(a.get(k, 0) - b.get(k, 0)) <= max(100, 0.10 * max(a.get(k, 0), 1)) for k in TACH_KEYS)
|
||||
|
||||
|
||||
@test("cooling.fans-quiet-after-motion", title="Fan profile returns to idle after motion and after M8/M9",
|
||||
subsystem="cooling", kind="auto", mode="grbl", est_min=3,
|
||||
@test("cooling.fans-quiet-after-motion", title="Fan profile returns to idle after motion, after M8/M9, "
|
||||
"and after a daemon restart on a busy machine",
|
||||
subsystem="cooling", kind="auto", mode="grbl", est_min=5,
|
||||
covers=_COOL_COVERS + [("forgectrl", "src/super.c")], requires=["motion.pacing"],
|
||||
steps=["Bed clear; the head needs 20 mm of free +X travel."],
|
||||
description="A dry jog and an M8/M9 cycle must not leave the run fan profile on: within "
|
||||
"the cooldown the engine is back at its idle duty and the exhaust/intake tachs "
|
||||
"are back at (or below) the idle level they held before the test. The idle "
|
||||
"reference is taken only once the engine is idle and the tachs have stopped "
|
||||
"changing, so a previous test's spin-down cannot be mistaken for idle.")
|
||||
"changing, so a previous test's spin-down cannot be mistaken for idle. Then a "
|
||||
"forgectrl restart with the kernel not idle (a takeover's safe state): the "
|
||||
"engine starts in its cooldown posture and must take the idle duties back once "
|
||||
"the machine is idle, not keep the exhaust at cooldown duty for good.")
|
||||
def fans_quiet(ctx):
|
||||
fc = ctx.forgectrl
|
||||
ev = ctx.evidence
|
||||
@@ -238,6 +243,35 @@ def fans_quiet(ctx):
|
||||
ctx.check(settle is not None, "fans did not return to the idle profile within %d s: %s, duty %s, "
|
||||
"phase %s (idle reference %s)", COOLDOWN_TIMEOUT_S, ev["after"], ev["duty_after"], phase(), before)
|
||||
|
||||
# A daemon restart while the kernel is not idle: forgectrl stopped, the
|
||||
# kernel put in the takeover's safe state (disabled), forgectrl started.
|
||||
# The engine judges the machine busy and takes the cooldown airflow; the
|
||||
# controller's start enables the kernel and the machine is idle again,
|
||||
# and the engine must follow it to the idle duties.
|
||||
rc, out = hw.initd("forgectrl", "stop")
|
||||
ctx.log("forgectrl stop -> rc %s", rc)
|
||||
ctx.check(rc == 0, "forgectrl stop failed: %s", (out or "").strip()[:200])
|
||||
hw.sysfs_write("cnc/disable", "1")
|
||||
ev["kstate_at_restart"] = hw.sysfs_read("cnc/state")
|
||||
rc, out = hw.initd("forgectrl", "start")
|
||||
ctx.log("forgectrl start with cnc/state=%s -> rc %s", ev["kstate_at_restart"], rc)
|
||||
ctx.check(rc == 0, "forgectrl start failed: %s", (out or "").strip()[:200])
|
||||
back = None
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < RESTART_IDLE_TIMEOUT_S:
|
||||
ctx.sleep(SAMPLE_S)
|
||||
st, m = fc.get("/mode")
|
||||
d = _duties()
|
||||
ctrl = m.get("controller") if isinstance(m, dict) else st
|
||||
ctx.log(" after the restart +%3.0f s: controller %s duty %s", time.time() - t0, ctrl, d)
|
||||
if ctrl == "running" and d == IDLE_DUTY:
|
||||
back = time.time() - t0
|
||||
break
|
||||
ev["restart_idle_s"] = round(back, 1) if back is not None else None
|
||||
ev["duty_after_restart"] = _duties()
|
||||
ctx.check(back is not None, "the engine kept the busy-start airflow after the restart (duty %s "
|
||||
"after %d s)", ev["duty_after_restart"], RESTART_IDLE_TIMEOUT_S)
|
||||
|
||||
|
||||
GATE_KEYS = ("cool_temp_max", "cool_temp_resume")
|
||||
VERDICT_WAIT_S = 20 # the engine reloads settings at run start and ticks at 1 Hz
|
||||
|
||||
@@ -89,19 +89,38 @@ class FansQuietTests(unittest.TestCase):
|
||||
self.tmp = tempfile.mkdtemp(prefix="forgetest-cool-")
|
||||
self.sysfs = os.path.join(self.tmp, "sysfs") + os.sep
|
||||
os.makedirs(self.sysfs + "thermal")
|
||||
os.makedirs(self.sysfs + "cnc")
|
||||
with open(self.sysfs + "cnc/state", "w") as f:
|
||||
f.write("idle")
|
||||
os.environ["GF_SYSFS_ROOT"] = self.sysfs
|
||||
self.fc = helpers.FakeForgectrl().start()
|
||||
self.grbl = FakeGrbl()
|
||||
self.saved = (cooling.SAMPLE_S, cooling.IDLE_REF_TIMEOUT_S, cooling.COOLDOWN_TIMEOUT_S)
|
||||
self.saved = (cooling.SAMPLE_S, cooling.IDLE_REF_TIMEOUT_S, cooling.COOLDOWN_TIMEOUT_S,
|
||||
cooling.RESTART_IDLE_TIMEOUT_S, cooling.hw.initd)
|
||||
cooling.SAMPLE_S = 0.1
|
||||
cooling.IDLE_REF_TIMEOUT_S = 3
|
||||
cooling.COOLDOWN_TIMEOUT_S = 4
|
||||
cooling.RESTART_IDLE_TIMEOUT_S = 1
|
||||
# The daemon restart of the last phase, scripted: the init script
|
||||
# is a stub, and the engine that comes up applies `after_start`
|
||||
# (the idle duties by default; a daemon that keeps the busy-start
|
||||
# cooldown airflow applies those instead).
|
||||
self.initd_calls = []
|
||||
self.after_start = (0, 0)
|
||||
|
||||
def initd(service, action, timeout=60):
|
||||
self.initd_calls.append((service, action))
|
||||
if action == "start":
|
||||
self.duty(*self.after_start)
|
||||
return 0, ""
|
||||
cooling.hw.initd = initd
|
||||
self.fc.state["cool"] = {"phase": "idle", "armed": False, "hold": False}
|
||||
self.duty(0, 0)
|
||||
self.fans(0, 736, 733)
|
||||
|
||||
def tearDown(self):
|
||||
cooling.SAMPLE_S, cooling.IDLE_REF_TIMEOUT_S, cooling.COOLDOWN_TIMEOUT_S = self.saved
|
||||
(cooling.SAMPLE_S, cooling.IDLE_REF_TIMEOUT_S, cooling.COOLDOWN_TIMEOUT_S,
|
||||
cooling.RESTART_IDLE_TIMEOUT_S, cooling.hw.initd) = self.saved
|
||||
self.grbl.close()
|
||||
self.fc.stop()
|
||||
os.environ.pop("GF_SYSFS_ROOT", None)
|
||||
@@ -174,6 +193,26 @@ class FansQuietTests(unittest.TestCase):
|
||||
self.assertIsNotNone(run.evidence["settle_s"])
|
||||
self.assertTrue(any("idle ref:" in ln for ln in run.lines))
|
||||
|
||||
def test_the_restart_phase_stops_disables_starts_and_sees_idle_duty(self):
|
||||
self.run_engine()
|
||||
run = self.run_test()
|
||||
self.assertEqual(run.finished, None)
|
||||
self.assertEqual(self.initd_calls, [("forgectrl", "stop"), ("forgectrl", "start")])
|
||||
with open(self.sysfs + "cnc/disable") as f:
|
||||
self.assertEqual(f.read(), "1")
|
||||
self.assertEqual(run.evidence["duty_after_restart"], cooling.IDLE_DUTY)
|
||||
self.assertIsNotNone(run.evidence["restart_idle_s"])
|
||||
|
||||
def test_a_daemon_that_keeps_the_busy_start_airflow_fails(self):
|
||||
# The bench case: after a takeover's restart the exhaust ran at
|
||||
# cooldown duty on an idle machine for good.
|
||||
self.run_engine()
|
||||
self.after_start = (32768, 21639)
|
||||
with self.assertRaises(Failed) as cm:
|
||||
self.run_test()
|
||||
self.assertIn("kept the busy-start airflow", str(cm.exception))
|
||||
self.assertIn("32768", str(cm.exception))
|
||||
|
||||
def test_fans_left_running_fail_with_the_reference_in_the_message(self):
|
||||
def on_command(line):
|
||||
if line == "M8":
|
||||
|
||||
Reference in New Issue
Block a user