mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 08:41:13 -07:00
forgetest: cloud.verdict-hold drills the warm-up wait of an armed print
The start gate set just above the coolant opens the armed session under the warm-up; the cloud client waits it out after the button, nothing runs, and the release starts the print, which completes. The mid-run hold and its bound are host-tested: the gates apply at session open, so no setting can produce a hold mid-run on the bench. Bench-excerpt unit tests: the pass, and the failure when a run starts under the hold.
This commit is contained in:
@@ -1289,6 +1289,94 @@ def lid_during_button_wait_body(ctx, ev, off, job, offset):
|
|||||||
"button dark, ring empty with the feed stopped")
|
"button dark, ring empty with the feed stopped")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- the cooling verdict
|
||||||
|
|
||||||
|
HOLD_KEYS = ("cool_temp_min", "cool_temp_start")
|
||||||
|
WARMUP_ABOVE_C = 1.0 # the start gate this far above the loop: a few heater minutes
|
||||||
|
WARMUP_RELEASE_S = 720 # the flow heater warms the bulk 0.4 to 0.8 C a minute
|
||||||
|
WARMUP_WAIT_LINE = "waiting on the cooling engine: WARMUP"
|
||||||
|
WARMUP_RELEASE_LINE = "cooling verdict clean after WARMUP; starting the run"
|
||||||
|
|
||||||
|
|
||||||
|
@test("cloud.verdict-hold", title="A cloud print armed under the warm-up gate waits for the engine",
|
||||||
|
subsystem="cloud", kind="operator", est_min=14,
|
||||||
|
covers=_MACHINE_RUN + [("python3-gfhardware", "gfhardware/coolsvc.py"),
|
||||||
|
("forgectrl", "src/cool.*"), ("forgectrl", "src/main.c")],
|
||||||
|
requires=["cloud.lid-during-button-wait", "cooling.floor-and-warm-up"], actions=["button"],
|
||||||
|
steps=[OFFLINE_STEP,
|
||||||
|
"Coolant at room temperature (10 to 30 C). The test sets the warm-up gate just above "
|
||||||
|
"the coolant and restores it. Press the button when it lights: the print then waits "
|
||||||
|
"for the warm-up (a few minutes with the loop heater on), runs dark, and finishes."],
|
||||||
|
description="The cooling-engine contract in cloud mode, on the engine itself: the start gate "
|
||||||
|
"set just above the coolant makes the armed session open under the warm-up "
|
||||||
|
"(WARMUP: fire blocked, hold), and the client waits it out after the button "
|
||||||
|
"instead of canceling - nothing runs, the latch stays unlocked for the armed "
|
||||||
|
"window - until the engine releases into run, and the release starts the print, "
|
||||||
|
"which completes. The hold mid-run (the same laser-off pause as the button's, "
|
||||||
|
"resumed on the engine's resume_ok) and the bound on a hold are host-tested: the "
|
||||||
|
"gates apply at session open, so no setting can produce a hold mid-run here.")
|
||||||
|
def verdict_hold(ctx):
|
||||||
|
ev = ctx.evidence
|
||||||
|
fc = ctx.forgectrl
|
||||||
|
before = fc.settings()
|
||||||
|
orig = {k: before.get(k, "") for k in HOLD_KEYS}
|
||||||
|
ev["orig"] = orig
|
||||||
|
up = (fc.status().get("coolant") or {}).get("up_c")
|
||||||
|
ctx.check(up is not None and 10.0 <= up <= 30.0, "coolant outside the test's range (up_c %s)", up)
|
||||||
|
offset = enter_offline(ctx)
|
||||||
|
job = offline_job(ctx, "hold.puls", seconds=30)
|
||||||
|
off = Offline().__enter__()
|
||||||
|
try:
|
||||||
|
verdict_hold_body(ctx, ev, off, job, offset, fc, up, orig)
|
||||||
|
finally:
|
||||||
|
st, body = fc.post("/settings", params=orig)
|
||||||
|
ctx.log("restore: POST /settings %s -> %s", orig, st)
|
||||||
|
off.__exit__(None, None, None)
|
||||||
|
offline_cleanup(ctx)
|
||||||
|
|
||||||
|
|
||||||
|
def verdict_hold_body(ctx, ev, off, job, offset, fc, up, orig):
|
||||||
|
gate = round(up + WARMUP_ABOVE_C, 1)
|
||||||
|
st, body = fc.post("/settings", params={"cool_temp_start": str(gate),
|
||||||
|
"cool_temp_min": orig["cool_temp_min"]})
|
||||||
|
ctx.check(st == 200, "POST /settings cool_temp_start=%s -> %s %s", gate, st, body)
|
||||||
|
ev["gate"] = gate
|
||||||
|
off.print_ready(9005, job)
|
||||||
|
got = wait_log(ctx, offset, ["waiting for button"], 120)
|
||||||
|
ctx.check(got["waiting for button"], "the print never reached the button wait")
|
||||||
|
ctx.act("button", "press", text="The button is lit: press it. The print then waits for the warm-up.",
|
||||||
|
until=lambda: log_has(offset, WARMUP_WAIT_LINE), timeout=PRESS_TIMEOUT_S)
|
||||||
|
got = wait_log(ctx, offset, [WARMUP_WAIT_LINE], 30)
|
||||||
|
ctx.check(got[WARMUP_WAIT_LINE], "the armed print did not wait on the warm-up")
|
||||||
|
st, c = fc.get("/cool/status")
|
||||||
|
ev["warmup"] = c
|
||||||
|
ctx.check(isinstance(c, dict) and c.get("verdict") == "WARMUP" and c.get("hold") is True
|
||||||
|
and c.get("fire_ok") is False,
|
||||||
|
"the engine is not holding the armed session on the warm-up: %s", c)
|
||||||
|
ctx.check(wait_print_running(ctx, offset, 2) is None, "the run started under the warm-up hold")
|
||||||
|
ctx.notice("Warm-up: the loop heater brings the coolant to the gate; a few minutes. Do nothing.")
|
||||||
|
try:
|
||||||
|
t0 = time.time()
|
||||||
|
run = wait_print_running(ctx, offset, WARMUP_RELEASE_S)
|
||||||
|
finally:
|
||||||
|
ctx.clear_notice()
|
||||||
|
ev["release_s"] = round(time.time() - t0)
|
||||||
|
got = wait_log(ctx, offset, [WARMUP_RELEASE_LINE], 5)
|
||||||
|
ev["log"] = {WARMUP_WAIT_LINE: True, WARMUP_RELEASE_LINE: bool(got[WARMUP_RELEASE_LINE])}
|
||||||
|
ctx.check(got[WARMUP_RELEASE_LINE], "the warm-up did not release into the run within %d s",
|
||||||
|
WARMUP_RELEASE_S)
|
||||||
|
ctx.check(run is not None, "the print did not start after the warm-up")
|
||||||
|
fin = wait_action_finished(ctx, offset, "print", 180)
|
||||||
|
ev["print finished"] = message(fin)
|
||||||
|
ctx.check(fin and COMPLETED in fin, "the print did not complete after the warm-up: %s",
|
||||||
|
message(fin) or "no finish line")
|
||||||
|
st, cs = fc.get("/cool/status")
|
||||||
|
ev["armed_after"] = cs.get("armed") if isinstance(cs, dict) else None
|
||||||
|
ctx.check(not ev["armed_after"], "armed window still open after the print")
|
||||||
|
ev["events"] = offline_events(off)
|
||||||
|
ctx.log("PASS: the armed print waited %s s on the warm-up, then ran and completed", ev["release_s"])
|
||||||
|
|
||||||
|
|
||||||
@test("cloud.pause-resume", title="Button pauses and resumes a cloud print (factory backtrack + lead)",
|
@test("cloud.pause-resume", title="Button pauses and resumes a cloud print (factory backtrack + lead)",
|
||||||
subsystem="cloud", kind="live", est_min=8,
|
subsystem="cloud", kind="live", est_min=8,
|
||||||
covers=_CLOUD_ALL,
|
covers=_CLOUD_ALL,
|
||||||
|
|||||||
@@ -912,6 +912,66 @@ class CloudSuiteTests(unittest.TestCase):
|
|||||||
self.assertEqual(self.fc.posts, [])
|
self.assertEqual(self.fc.posts, [])
|
||||||
self.assertTrue(any("PASS: lid open at the button prompt" in l for l in run.lines))
|
self.assertTrue(any("PASS: lid open at the button prompt" in l for l in run.lines))
|
||||||
|
|
||||||
|
# -- the cooling verdict: the armed print waits on the warm-up ------------
|
||||||
|
def test_verdict_hold_on_the_bench_excerpt(self):
|
||||||
|
self.in_offline()
|
||||||
|
self.fc.state["status"]["coolant"] = {"up_c": 21.5, "down_c": 21.4}
|
||||||
|
self.fc.state["settings"].update({"cool_temp_min": "5", "cool_temp_start": "16"})
|
||||||
|
self.fc.state["cool"].update({"verdict": "OK", "hold": False, "fire_ok": True, "armed": False})
|
||||||
|
lines = fixture("pause")
|
||||||
|
pre, rest = cut(lines, "waiting for button")
|
||||||
|
pre = pre + [rest[0]]
|
||||||
|
# the run and the print's end, without the pause in the middle
|
||||||
|
_, run = cut(rest, "machine:_run_loop starting run")
|
||||||
|
run = [l for l in run if "paus" not in l and "resum" not in l]
|
||||||
|
stamp = "2026-08-17T09:44:14.%06d+00:00 gfcloud[1927] INFO machine:_verdict_wait "
|
||||||
|
wait = [stamp % 100000 + "waiting on the cooling engine: WARMUP (WARM-UP: coolant 21.5 C under "
|
||||||
|
"the 22.5 C start gate - heater on, hold)"]
|
||||||
|
release = [stamp % 200000 + "cooling verdict clean after WARMUP; starting the run"]
|
||||||
|
|
||||||
|
def on_post(path, form):
|
||||||
|
if path == "/settings" and form.get("cool_temp_start") not in (None, "0", "16"):
|
||||||
|
self.fc.state["cool"].update({"verdict": "WARMUP", "hold": True, "fire_ok": False,
|
||||||
|
"armed": True, "phase": "warm-up"})
|
||||||
|
return None
|
||||||
|
self.fc.on_post = on_post
|
||||||
|
self.offline_print_hooks(pre)
|
||||||
|
|
||||||
|
def press():
|
||||||
|
self.append(wait, delay=0.05)
|
||||||
|
|
||||||
|
def released():
|
||||||
|
self.fc.state["cool"].update({"verdict": "OK", "hold": False, "fire_ok": True,
|
||||||
|
"armed": False, "phase": "run"})
|
||||||
|
self.append(release + run, delay=0.0)
|
||||||
|
threading.Timer(3.0, released).start()
|
||||||
|
hooks = {"press it. The print then waits": press}
|
||||||
|
run_ = self.run_test(cloud.verdict_hold, hooks=hooks, test_id="cloud.verdict-hold")
|
||||||
|
ev = run_.evidence
|
||||||
|
self.assertEqual(ev["gate"], 22.5)
|
||||||
|
self.assertEqual(ev["warmup"]["verdict"], "WARMUP")
|
||||||
|
self.assertTrue(ev["log"]["cooling verdict clean after WARMUP; starting the run"])
|
||||||
|
self.assertIn(":completed", ev["print finished"])
|
||||||
|
posted = [f for p, f in self.fc.posts if p == "/settings"]
|
||||||
|
self.assertEqual(posted[0]["cool_temp_start"], "22.5")
|
||||||
|
self.assertEqual(posted[-1], {"cool_temp_min": "5", "cool_temp_start": "16"})
|
||||||
|
self.assertTrue(any("PASS: the armed print waited" in l for l in run_.lines))
|
||||||
|
|
||||||
|
def test_verdict_hold_fails_when_the_print_runs_under_the_hold(self):
|
||||||
|
self.in_offline()
|
||||||
|
self.fc.state["status"]["coolant"] = {"up_c": 21.5, "down_c": 21.4}
|
||||||
|
self.fc.state["settings"].update({"cool_temp_min": "5", "cool_temp_start": "16"})
|
||||||
|
self.fc.state["cool"].update({"verdict": "WARMUP", "hold": True, "fire_ok": False, "armed": True})
|
||||||
|
lines = fixture("pause")
|
||||||
|
pre, rest = cut(lines, "waiting for button")
|
||||||
|
pre = pre + [rest[0]]
|
||||||
|
_, run = cut(rest, "machine:_run_loop starting run")
|
||||||
|
stamp = "2026-08-17T09:44:14.%06d+00:00 gfcloud[1927] INFO machine:_verdict_wait "
|
||||||
|
wait = [stamp % 100000 + "waiting on the cooling engine: WARMUP (no reason given)"]
|
||||||
|
self.offline_print_hooks(pre)
|
||||||
|
hooks = {"press it. The print then waits": lambda: self.append(wait + run[:1], delay=0.05)}
|
||||||
|
self.assertFails(cloud.verdict_hold, "the run started under the warm-up hold", hooks=hooks)
|
||||||
|
|
||||||
# -- a paused print cancelled by the lid, a running one by the app --------
|
# -- a paused print cancelled by the lid, a running one by the app --------
|
||||||
def cancel_parts(self):
|
def cancel_parts(self):
|
||||||
"""(print prologue, the pause lines, the lid stop + park + cancel,
|
"""(print prologue, the pause lines, the lid stop + park + cancel,
|
||||||
|
|||||||
Reference in New Issue
Block a user