From 5d26818946b9b9fb4f05afb79475266aa08a20fd Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Fri, 16 Oct 2026 14:21:24 -0400 Subject: [PATCH] forgetest: a press the fixture made and reported failed leaves no notice standing press_now and arm_press hand the arm press to the operator when the bench actuator reports that the press failed. When the box makes its 200 ms press and its reply is lost, the client raises that failure at its 3 s timeout or later (with ip set in the config it does not retry), and by then the controller has taken the press and put the button out. The fallback put the notice up all the same and started the watch, which waits for the button to light before it waits for it to go out. The button does not light again, so "Press it now" stood through the job and asked for a second press, and a press during the job pauses it. Both fallbacks now record the fixture's error and then read the button. Out: the press was taken, it is recorded as the fixture's, and no notice goes up. Lit, or unreadable: the notice goes up with its watch, as before. Nothing presses for the operator, and no 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 and on Windows with python 3.14: test_fixture.py's test_the_arm_notice_comes_down_when_the_press_is_taken gains the leg where the box puts the button out and then reports a failure: press_now returns True with no notice and the press recorded as the fixture's, and arm_press leaves no notice and the same record. The file's 21 tests OK on Ubuntu, and on Windows with test_ready_reads_a_short_press_as_an_edge skipped (no os.mkfifo). test_setup_burns.py (22), test_setup_dark.py (20), test_server.py (10), test_operator.py (17), test_queue.py (15) and test_mode.py (9) OK on Ubuntu. Negative controls on scratch copies: the leg against the runner as it was fails at press_now (False, the notice standing); with arm_press's fallback alone as it was, the leg fails at its record ('operator', the notice up). 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 | 19 +++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/forgetest/forgetest/runner.py b/forgetest/forgetest/runner.py index 27cd4fe..75f6db5 100644 --- a/forgetest/forgetest/runner.py +++ b/forgetest/forgetest/runner.py @@ -464,8 +464,9 @@ class Context: The fixture presses where the bench took this test's presses (or opted in standing); otherwise the notice goes up for a person, - who is looking at the same prompt. Returns True when the fixture - pressed.""" + who is looking at the same prompt. A press the fixture reports + failed while the button went out was taken all the same, and no + notice goes up for it. Returns True when the fixture pressed.""" fixture = getattr(self.runner, "fixture", None) if self.runner is not None else None rec = {"channel": "button", "state": "arm", "by": "operator", "ts": now_ts()} self.evidence.setdefault("actions", []).append(rec) @@ -483,6 +484,8 @@ class Context: fixture.act("button", "press") except _fixture.FixtureError as e: rec["fixture_error"] = str(e) + if self._press_taken(rec, e): + return True self.log("ARM press: fixture failed (%s) - asking the operator", e) self.notice(text) self._clear_when_taken() @@ -491,6 +494,20 @@ class Context: self.log("ARM press by the fixture, at the machine's own press prompt") return True + def _press_taken(self, rec, err): + """After the box reported a failed press of a lit button: True when + the button is out, so the controller took a press (the box pressed + and its reply was lost). The press is recorded as the box's, and + no notice goes up, because it would ask for a second press, and a + press during the job pauses it. A button still lit, or unreadable, + is False: the operator is asked.""" + if hw.button_lit() is not False: + return False + rec["by"] = "fixture" + self.log("ARM press: the fixture reported a failure (%s), and the button is out: the press " + "was taken; no notice", err) + return True + def _clear_when_taken(self, lit_timeout=ARM_LIT_WAIT_S): """Take the arm notice down the moment the press is taken: the controller lights the button white for its arm wait and puts it out @@ -541,8 +558,9 @@ class Context: seconds for the button to light (the job may still be on its way to the arm wait; a card that settles the coolant first takes minutes) and presses once, recorded as the fixture's; if the - button never lights or the press fails, the notice goes up for a - person. Returns True when the fixture has been asked.""" + button never lights, or the press fails with the button still lit, + the notice goes up for a person. Returns True when the fixture has + been asked.""" fixture = getattr(self.runner, "fixture", None) if self.runner is not None else None rec = {"channel": "button", "state": "arm", "by": "operator", "ts": now_ts()} self.evidence.setdefault("actions", []).append(rec) @@ -572,8 +590,11 @@ class Context: try: fixture.act("button", "press") except _fixture.FixtureError as e: - self.log("ARM press: fixture failed (%s) - asking the operator", e) rec["fixture_error"] = str(e) + if self._press_taken(rec, e): + rec["took_s"] = round(lit, 2) + return + self.log("ARM press: fixture failed (%s) - asking the operator", e) self.notice(text) self._clear_when_taken() return diff --git a/forgetest/tests/test_fixture.py b/forgetest/tests/test_fixture.py index 9f7d2ae..a0ad57a 100644 --- a/forgetest/tests/test_fixture.py +++ b/forgetest/tests/test_fixture.py @@ -626,6 +626,25 @@ class RoutingTests(unittest.TestCase): time.sleep(0.2) lit["v"] = False self.assertTrue(settles(False)) + # A press the box made and reported failed (its reply lost after + # the press): the button is out when the error arrives, so the + # press was taken, and no notice asks for a second one. + def pressed_then_failed(channel, state, ms=None): + lit["v"] = False # the controller takes the press + raise fx.FixtureError("timed out") + self.stub.act = pressed_then_failed + lit["v"] = True + self.assertTrue(ctx.press_now()) + self.assertIsNone(run.notice) + self.assertEqual(ctx.evidence["actions"][-1]["by"], "fixture") + lit["v"] = True + self.assertTrue(ctx.arm_press()) + deadline = time.time() + 2 + while ctx.evidence["actions"][-1]["by"] != "fixture" and time.time() < deadline: + time.sleep(0.02) + time.sleep(0.1) + self.assertEqual(ctx.evidence["actions"][-1]["by"], "fixture") + self.assertIsNone(run.notice) finally: runner_mod.hw.button_lit = saved