From 3d2f2d9ab49bcdda88973fa4acacaebec0e19a72 Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Thu, 15 Oct 2026 18:01:31 -0400 Subject: [PATCH] forgetest: the arm-notice watch ends with its notice and the run _clear_when_taken takes the operator's arm notice down the moment the controller takes the press (the button lights for the arm wait and goes out at the press). Its watch thread polled the button LEDs every 50 ms with the arm's timeouts as its only exits: 30 minutes for the button to light, 90 minutes in all. Nothing in it read the run's end, the abort event, or whether the notice it guarded still stood, so a test that ended before the press (a FAIL on the way to the arm) left a thread reading up to six sysfs files twenty times a second behind it. The watch now lives only as long as the notice it guards and the run: a notice cleared or replaced, a run that finished, or an abort ends it. `standing()` reads both under the run's lock on every poll, and the poll waits on the run's abort event instead of sleeping, so an abort ends the wait at once. The cadence stays 50 ms: the notice still drops within 50 ms of the press. The watch commands nothing; no press, judge, or energy path changes. Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s tests -v` in forgetest), on Ubuntu 24.04 with python 3.12: test_fixture.py's test_the_arm_notice_comes_down_when_the_press_is_taken gains the leg that a run finished before the press ends the watch within 2 s and leaves the finished run's notice as it was; the file's 21 tests pass; test_setup_burns.py, whose burns reach the watch through press_now, passes its 19 tests and leaves no forgetest-arm-notice thread behind (10 before this change); and test_server.py, test_operator.py and test_queue.py pass. Negative control on a scratch copy: the new leg against the unchanged runner fails with the forgetest-arm-notice thread still alive after the run finished. pyflakes reports no undefined name. Acceptance: every catalog test's fingerprint moves: runner.py is a shared judge, in every implementation hash (43 of 43 move). Against the campaign this series already needs, that adds no run: 850498f moved all 43 once. tests/ are in none. --- forgetest/forgetest/runner.py | 31 +++++++++++++++++++++---------- forgetest/tests/test_fixture.py | 18 ++++++++++++++++++ 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/forgetest/forgetest/runner.py b/forgetest/forgetest/runner.py index 906996c..7a76787 100644 --- a/forgetest/forgetest/runner.py +++ b/forgetest/forgetest/runner.py @@ -495,26 +495,37 @@ class Context: controller lights the button white for its arm wait and puts it out at the press. Without this the notice stood until the test's next step, and an operator who still saw it pressed again, which pauses - the job. A notice replaced meanwhile is left alone.""" - with self.run._lock: - nid = (self.run.notice or {}).get("id") + the job. A notice replaced meanwhile is left alone. The watch lives + only as long as the notice it guards and the run: a notice cleared + or replaced, a run that finished (passed, failed, or errored before + the press) or an abort ends it, so no thread is left polling the + LEDs behind a test.""" + run = self.run + with run._lock: + nid = (run.notice or {}).get("id") if nid is None: return + def standing(): + with run._lock: + return run.finished is None and (run.notice or {}).get("id") == nid + def watch(): t0 = time.time() while hw.button_lit() is not True: - if time.time() - t0 > lit_timeout: + if not standing() or time.time() - t0 > lit_timeout: + return + if run.aborted.wait(0.05): return - time.sleep(0.05) while hw.button_lit() is not False: - if time.time() - t0 > lit_timeout + ARM_PRESS_WAIT_S: + if not standing() or time.time() - t0 > lit_timeout + ARM_PRESS_WAIT_S: return - time.sleep(0.05) - with self.run._lock: - same = (self.run.notice or {}).get("id") == nid + if run.aborted.wait(0.05): + return + with run._lock: + same = (run.notice or {}).get("id") == nid if same: - self.run.notice = None + run.notice = None if same: self.log("ARM: the button went out at the press; the notice is down") diff --git a/forgetest/tests/test_fixture.py b/forgetest/tests/test_fixture.py index 46e5150..0e114fb 100644 --- a/forgetest/tests/test_fixture.py +++ b/forgetest/tests/test_fixture.py @@ -576,6 +576,24 @@ class RoutingTests(unittest.TestCase): lit["v"] = False time.sleep(0.3) self.assertEqual((run.notice or {}).get("text"), "Something else.") + # The watch ends with the run: a test that ends before the press + # (a FAIL on the way to the arm, say) leaves no thread polling the + # LEDs for up to the arm's whole timeout. + def watching(): + return [t for t in threading.enumerate() if t.name == "forgetest-arm-notice"] + deadline = time.time() + 2 + while watching() and time.time() < deadline: + time.sleep(0.02) # the earlier watches are gone + self.assertFalse(watching()) + lit["v"] = False + self.assertFalse(ctx.arm_press()) + self.assertTrue(watching()) # up while the arm is still dark + run.finished = {"result": "FAIL", "message": "", "duration_s": 0} + deadline = time.time() + 2 + while watching() and time.time() < deadline: + time.sleep(0.02) + self.assertFalse(watching()) + self.assertIsNotNone(run.notice) # the finished run's notice is left as it was finally: runner_mod.hw.button_lit = saved