forgetest: cooling.intake-one-fan, and setup.check-airflow reads the intake fan count

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).
This commit is contained in:
ScottW514
2026-10-10 17:33:43 -04:00
parent 72907f311a
commit 16a8ae9ba3
5 changed files with 425 additions and 6 deletions
+119
View File
@@ -137,5 +137,124 @@ class SensorsCheckTests(unittest.TestCase):
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()