mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-10-16 01:29:55 -07:00
forgectrl c274f19 adds cool_intake_fans: at 1, a machine built with one
intake fan, the second intake is published not fitted and never judged,
and the setup's airflow check writes the count it finds.
cooling.intake-one-fan, in a suite module of its own, proves both halves
on a machine with two intake fans with the setting alone. With
cool_intake_fans 1 and an 8 s grace, a session at the cut fan profile
must publish intake_2 as not fitted with no floor while it reads a real
speed, run OK, and judge intake_1 ok at the intake floor; with the intake
floor at its legal maximum it must trip AIRFLOW (hold, fire blocked) with
intake_1 TRIPPED and intake_2 still not fitted, and the fault must end
with the session; restored, every fan reads ok at its floor. It starts
only from a machine with both intakes judged, requires
cooling.fan-gate-trips, and puts the count, the intake floor and the
grace back as found on every exit path.
setup.check-airflow now reads the count from the result: 2 needs all
four fans at least 1000 rpm, 1 needs intake 2 reported {"fitted": false}
and the other three ok; either way the setting must read what the check
found, and the count is put back with the five floors.
Fingerprints: the new test has a module of its own, so no cooling test
moves; the setup.check-airflow edit is inside its function. Diffed across
the change: 127 -> 128 tests, one added, setup.check-airflow the only
one moved.
Proof: the touched host tests pass (test_cooling_intake_suite 4,
test_setup_dark 10, test_cooling_suite 25) and pyflakes is clean.
test_cooling_intake_suite replays the test against a scripted engine,
and two broken engines fail it with the settings put back: one that
ignores the setting, and one that takes intake 1 out with intake 2.
test_setup_dark's new AirflowCheckTests take a two-fan and a one-fan
result, and fail a count the setting does not match and a result with
no count. On the bench reference, image 20260928194539 (dev) with these
files and forgectrl c274f19 bind-mounted, cooling.fan-gate-trips,
cooling.intake-one-fan and setup.check-airflow pass with a clean
baseline before and after each (forgectrl's commit has the readings).
261 lines
11 KiB
Python
261 lines
11 KiB
Python
# Copyright 2026 514 LLC d/b/a OpenGlow
|
|
# Written by Scott Wiederhold
|
|
# https://community.openglow.org
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""The setup's checks in the catalog: registered as automatic tests with
|
|
no precheck, the takeover ones marked so, every covers map naming the
|
|
check runner; and the check driver itself against the fake daemon: a
|
|
start, the run polled to its end with no question asked, the result
|
|
taken, the record read at version 1, and the settings read as before."""
|
|
import os
|
|
import sys
|
|
import unittest
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
import helpers # noqa: E402
|
|
from forgetest import catalog # noqa: E402
|
|
from forgetest.runner import Context, Run # noqa: E402
|
|
|
|
IDS = ["setup.check-switches", "setup.check-sensors", "setup.check-airflow",
|
|
"setup.check-cameras", "setup.check-motion", "setup.check-flow-verify"]
|
|
|
|
|
|
class RegistrationTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.reg = catalog.load_suite()
|
|
|
|
def test_every_check_runs_unattended_with_the_fixture(self):
|
|
# The lid and button checks name their machine actions, which the
|
|
# bench fixture performs; the rest need no hands at all.
|
|
for tid in IDS:
|
|
self.assertIn(tid, self.reg)
|
|
for tid in ("setup.check-switches", "setup.check-cameras"):
|
|
self.assertEqual(self.reg[tid].kind, "operator", tid)
|
|
self.assertTrue(self.reg[tid].fixture_runnable(("button", "lid", "interlock")), tid)
|
|
for tid in ("setup.check-sensors", "setup.check-airflow", "setup.check-motion",
|
|
"setup.check-flow-verify"):
|
|
self.assertEqual(self.reg[tid].kind, "auto", tid)
|
|
|
|
def test_no_check_asks_the_operator_to_prepare_anything(self):
|
|
for tid in IDS:
|
|
self.assertIsNone(getattr(self.reg[tid], "precheck", None), tid)
|
|
|
|
def test_the_checks_that_take_the_machine_say_so(self):
|
|
for tid in ("setup.check-airflow", "setup.check-motion", "setup.check-flow-verify"):
|
|
self.assertEqual(self.reg[tid].hardware, "takeover", tid)
|
|
|
|
def test_every_check_covers_the_runner_and_the_page(self):
|
|
for tid in IDS:
|
|
covers = set(self.reg[tid].covers)
|
|
self.assertIn(("forgectrl", "src/wizdark.*"), covers, tid)
|
|
self.assertIn(("forgectrl", "src/ui/wizard.*"), covers, tid)
|
|
|
|
|
|
class SensorsCheckTests(unittest.TestCase):
|
|
"""The check driver against a scripted daemon."""
|
|
|
|
RESULT = {"coolant_down_c": 22.4, "coolant_up_c": 21.9, "chassis_c": 28.0, "soc_c": 45.2,
|
|
"lid_ir_max": [110, 120, 105, 118], "accel_events": 0, "laser_pgood": 1,
|
|
"hv_current_max": 0, "exhaust_rpm_idle": 3400, "intake_rpm_idle": 2200}
|
|
|
|
def setUp(self):
|
|
self.fake = helpers.FakeForgectrl().start()
|
|
self.fake.state["settings"].update({"cool_temp_offset_c": "0.5"})
|
|
self.dark = {"id": "", "running": False, "phase": "", "progress": 0, "elapsed_s": 0,
|
|
"log": [], "prompt": None, "result": None, "error": "",
|
|
"shots": {"lid": False, "head": False}}
|
|
self.versions = {}
|
|
self.answers = []
|
|
self.polls = 0
|
|
|
|
def on_get(path, q):
|
|
if path == "/wiz/dark":
|
|
# One poll sees the sampling; the next sees the result.
|
|
if self.dark["running"]:
|
|
self.polls += 1
|
|
if self.polls >= 2:
|
|
self.dark.update({"running": False, "prompt": None, "progress": 100,
|
|
"result": dict(self.RESULT)})
|
|
self.versions["sensors"] = 1
|
|
return 200, dict(self.dark)
|
|
if path == "/wiz":
|
|
return 200, {"versions": dict(self.versions), "required": [], "completed": True}
|
|
return None
|
|
|
|
def on_post(path, form):
|
|
s = self.fake.state["settings"]
|
|
if path == "/wiz/sensors/start":
|
|
self.dark.update({"id": "sensors", "running": True, "phase": "sampling",
|
|
"prompt": None})
|
|
return 200, {"started": True, "id": "sensors"}
|
|
if path == "/wiz/sensors/answer":
|
|
self.answers.append(dict(form))
|
|
return 409, {"error": "no such prompt is open"}
|
|
if path == "/settings":
|
|
for k, v in form.items():
|
|
if v == "":
|
|
s.pop(k, None)
|
|
else:
|
|
s[k] = v
|
|
return 200, dict(s)
|
|
return None
|
|
self.fake.on_get = on_get
|
|
self.fake.on_post = on_post
|
|
|
|
def tearDown(self):
|
|
self.fake.stop()
|
|
|
|
def test_start_no_question_result_and_settings_untouched(self):
|
|
t = catalog.load_suite()["setup.check-sensors"]
|
|
run = Run("test", t.id, t.title)
|
|
t.fn(Context(run, None, t))
|
|
self.assertIn(("/wiz/sensors/start", {}), [(p, f) for p, f in self.fake.posts])
|
|
self.assertEqual(self.answers, [])
|
|
self.assertEqual(run.evidence["result"]["laser_pgood"], 1)
|
|
self.assertEqual(run.evidence["settings_before"].get("cool_temp_offset_c"), "0.5")
|
|
self.assertEqual(self.fake.state["settings"]["cool_temp_offset_c"], "0.5")
|
|
self.assertNotIn("/settings", [p for p, _ in self.fake.posts])
|
|
|
|
def test_a_question_fails_the_check(self):
|
|
t = catalog.load_suite()["setup.check-sensors"]
|
|
run = Run("test", t.id, t.title)
|
|
real_get = self.fake.on_get
|
|
|
|
def on_get(path, q):
|
|
if path == "/wiz/dark" and self.dark["running"]:
|
|
d = dict(self.dark)
|
|
d["prompt"] = {"seq": 3, "id": "room-temp", "kind": "number", "text": "?",
|
|
"options": ["Set", "Skip"]}
|
|
return 200, d
|
|
return real_get(path, q)
|
|
self.fake.on_get = on_get
|
|
with self.assertRaises(Exception) as cm:
|
|
t.fn(Context(run, None, t))
|
|
self.assertIn("asked a question", str(cm.exception))
|
|
self.assertEqual(self.answers, [])
|
|
self.assertIn(("/wiz/sensors/abort", {}), [(p, f) for p, f in self.fake.posts])
|
|
|
|
|
|
class AirflowCheckTests(unittest.TestCase):
|
|
"""setup.check-airflow's judge against a scripted daemon: the check
|
|
writes its floors and the intake fans fitted, and the test reads the
|
|
fans the count says are there. A machine built with one intake fan
|
|
reports intake 2 not fitted; the bench reference has two."""
|
|
|
|
FLOORS = {"cool_tach_exhaust_min_rpm": "6400", "cool_tach_intake_min_rpm": "2290",
|
|
"cool_tach_air_assist_min_rpm": "6050", "cool_purge_min_current": "345",
|
|
"cool_fan_grace_s": "12"}
|
|
FAN = {"steady_rpm": 4150, "spinup_s": 6.8, "ok": True}
|
|
|
|
def result(self, intakes):
|
|
fans = {"exhaust": dict(self.FAN), "intake 1": dict(self.FAN), "intake 2": dict(self.FAN),
|
|
"air assist": dict(self.FAN)}
|
|
if intakes == 1:
|
|
fans["intake 2"] = {"fitted": False}
|
|
return {"fans": fans, "intake_fans": intakes, "purge_current_on": 628, "purge_current_off": 74,
|
|
"floors": dict(self.FLOORS)}
|
|
|
|
def setUp(self):
|
|
import tempfile
|
|
self.sysfs = tempfile.mkdtemp(prefix="forgetest-airflow-")
|
|
os.makedirs(os.path.join(self.sysfs, "head"))
|
|
with open(os.path.join(self.sysfs, "head", "purge_air"), "w") as f:
|
|
f.write("1\n")
|
|
self.saved_root = os.environ.get("GF_SYSFS_ROOT")
|
|
os.environ["GF_SYSFS_ROOT"] = self.sysfs + "/"
|
|
self.fake = helpers.FakeForgectrl().start()
|
|
self.fake.state["settings"].update({"cool_tach_intake_min_rpm": "2300"})
|
|
self.fake.state["cool"] = {"phase": "idle", "fan_gates": {"purge": {"reading": 628, "floor": 345}}}
|
|
self.dark = {"id": "", "running": False, "phase": "", "progress": 0, "elapsed_s": 0,
|
|
"log": [], "prompt": None, "result": None, "error": "",
|
|
"shots": {"lid": False, "head": False}}
|
|
self.versions = {}
|
|
self.polls = 0
|
|
self.wizard_result = self.result(2)
|
|
self.wizard_writes = None # what the check writes; None = what its result says
|
|
|
|
def on_get(path, q):
|
|
if path == "/wiz/dark":
|
|
if self.dark["running"]:
|
|
self.polls += 1
|
|
if self.polls >= 2:
|
|
r = self.wizard_result
|
|
s = self.fake.state["settings"]
|
|
s.update(r["floors"])
|
|
s["cool_intake_fans"] = self.wizard_writes or str(r["intake_fans"])
|
|
self.dark.update({"running": False, "progress": 100, "result": r})
|
|
self.versions["airflow"] = 1
|
|
return 200, dict(self.dark)
|
|
if path == "/wiz":
|
|
return 200, {"versions": dict(self.versions), "required": [], "completed": True}
|
|
return None
|
|
|
|
def on_post(path, form):
|
|
s = self.fake.state["settings"]
|
|
if path == "/wiz/airflow/start":
|
|
self.dark.update({"id": "airflow", "running": True, "phase": "fans to the run profile"})
|
|
return 200, {"started": True, "id": "airflow"}
|
|
if path == "/settings":
|
|
for k, v in form.items():
|
|
if v == "":
|
|
s.pop(k, None)
|
|
else:
|
|
s[k] = v
|
|
return 200, dict(s)
|
|
return None
|
|
self.fake.on_get = on_get
|
|
self.fake.on_post = on_post
|
|
|
|
def tearDown(self):
|
|
import shutil
|
|
self.fake.stop()
|
|
if self.saved_root is None:
|
|
os.environ.pop("GF_SYSFS_ROOT", None)
|
|
else:
|
|
os.environ["GF_SYSFS_ROOT"] = self.saved_root
|
|
shutil.rmtree(self.sysfs, ignore_errors=True)
|
|
|
|
def run_check(self):
|
|
t = catalog.load_suite()["setup.check-airflow"]
|
|
run = Run("test", t.id, t.title)
|
|
t.fn(Context(run, None, t))
|
|
return run
|
|
|
|
def assert_put_back(self):
|
|
s = self.fake.state["settings"]
|
|
self.assertEqual(s.get("cool_tach_intake_min_rpm"), "2300")
|
|
self.assertNotIn("cool_intake_fans", s)
|
|
self.assertNotIn("cool_tach_exhaust_min_rpm", s)
|
|
|
|
def test_two_intake_fans_pass_and_are_put_back(self):
|
|
self.run_check()
|
|
self.assert_put_back()
|
|
|
|
def test_one_intake_fan_passes_with_intake_2_not_fitted(self):
|
|
self.wizard_result = self.result(1)
|
|
self.run_check()
|
|
self.assert_put_back()
|
|
|
|
def test_one_intake_fan_with_the_setting_left_at_two_fails(self):
|
|
self.wizard_result = self.result(1)
|
|
self.wizard_writes = "2"
|
|
with self.assertRaises(Exception) as cm:
|
|
self.run_check()
|
|
self.assertIn("cool_intake_fans reads '2', the check found 1", str(cm.exception))
|
|
self.assert_put_back()
|
|
|
|
def test_a_result_without_the_count_fails(self):
|
|
r = self.result(2)
|
|
del r["intake_fans"]
|
|
self.wizard_result = r
|
|
self.wizard_writes = "2"
|
|
with self.assertRaises(Exception) as cm:
|
|
self.run_check()
|
|
self.assertIn("no intake fan count", str(cm.exception))
|
|
self.assert_put_back()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|