mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-10-15 17:19:55 -07:00
forgetest tests: the confirmed trims of the tool's own unit tests
The cases that tested a mock, a timer, a wording or a table, or that
replayed a judgment a host test already holds, leave forgetest/tests.
What catches a false PASS, keeps the bench safe, or guards the gate
stays.
- test_baseline: the fake sysfs tree is a mixin with no test_ methods,
so ExtensionFreeTests and BaselineModeTests no longer re-run
BaselineTests' 25 cases (133 runs, now 45). CoolPublishTests, the
lamp-fade cases and TransientNotLeftoverTests go; PositionDeadbandTests
keeps a reading past the band and an unreadable reading, both never
excused. Both latch-relock cases stay.
- test_fixture: MdnsTests, the three timing cases, the probe's log
wording, and nine prompt-routing variants go. A live test never runs
unattended, the arm-press opt-in, the short-press edge, the presence
press handing over the arm press, and the arm notice dropping at the
press stay (35 cases, now 21).
- test_cloud_suite: three false-PASS guards stay: the latch open at a
dark print's button, a lid stop that is not edge-driven, the kernel
refusing the resume. The guards for the latch unlocking in the hold
and a paused print resuming left with their catalog tests in 6a434bb.
The rest go, with the harness pieces and the hunt-lid fixture only
they read (41 cases, now 3).
- test_cooling_suite: GateOffTests, CriticalTierTests, FanGateTests and
FansQuietTests go with their harness; the flow verdict's log reads
stay: only the text after the offset, never an older verdict before it
(the flow-under-load false-PASS guard), a rotated log read whole.
- test_queue: RunClockTests and three ordering cases go, and refusals
are judged by a reason given, not its words; both live
acknowledgment cases stay.
- test_takeover_position: the six cases that judge the head and move
nothing stay.
- test_camera_home_return: the five cases where the head must not move
stay, and so do the three that check the hand-back jog's direction and
distance (homed, unhomed with the counters not counted twice, after
cloud mode): a wrong sign or a double count drives the head into the
gantry stops (15 cases, now 8).
- test_server: the keep-alive and ETag cases, the takeover and scope
half of the bench case, and the page's self-contained asserts
(test_page.py's) go.
- test_operator: the wording cases, the fixture case (test_fixture.py's)
and JournalTests go.
- test_mode: the supervisor timeout table goes.
- test_takeover_client: the listener, the offline service back, the
online client without the hunt, and GRBL under no marker stay.
- test_responsiveness: PageTests and ImplementationCostTests go; the
log cache keeps appends, a partial line, a replaced file and a fresh
list, and the fingerprint cache a changed manifest and one with no
content hash.
- test_bench_registry: GfbenchTests and two description and argument
pins go.
- test_installer: the log-format and TLS-wording cases and
InstallerShapeTests go; the die case checks the reason is in the log,
not its format.
- test_image: KernelIdentTests and the registration pin go.
Each file's __main__ guard now ends the file, and the docstrings say what
each still proves.
Proof: each of the fifteen files passes under Linux as forgetest-ci
discovers it, and pyflakes reports nothing new in them.
Acceptance: none; test-only.
This commit is contained in:
@@ -3,22 +3,18 @@
|
||||
# https://community.openglow.org
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""The cloud.* job-behavior tests against a fake forgectrl and a replayed
|
||||
gfcloud log: the real test functions run under the real runner Context,
|
||||
the operator prompts answered - and the standing notices of the machine
|
||||
actions acted on - by a script that also drives the replay (open the
|
||||
lid -> the machine's lid reads open; close it -> the service's re-hunt
|
||||
lines land). The log lines are the machine's own: bench excerpts
|
||||
(the lid-abort, button-wait, and lid-open-hunt runs) and, for the pause,
|
||||
the run loop's lines as its host test emits them on the print skeleton
|
||||
of the lid-abort excerpt.
|
||||
"""The cloud.* tests' false-PASS guards, against a fake forgectrl and a
|
||||
replayed gfcloud log: the real test functions run under the real runner
|
||||
Context, the operator prompts answered - and the standing notices of the
|
||||
machine actions acted on - by a script that also drives the replay. The
|
||||
log lines are the machine's own: the lid-abort bench excerpt, and for the
|
||||
pause, the run loop's lines as its host test emits them on the print
|
||||
skeleton of that excerpt.
|
||||
|
||||
What is proven here: the tests find the right lines in real log noise,
|
||||
reuse an existing cloud session and never switch back, restart the client
|
||||
for a fresh hunt, judge the print's own finish line (not another action's),
|
||||
wait the service's deferred moves out, and fail for the right reasons."""
|
||||
What is proven here: each test fails when the machine did the unsafe
|
||||
thing - a dark print with the latch open at the button wait, a lid stop
|
||||
that is polled rather than edge-driven, and a resume the kernel refused."""
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import struct
|
||||
@@ -28,7 +24,6 @@ import time
|
||||
import unittest
|
||||
|
||||
import helpers
|
||||
from forgetest import baseline
|
||||
from forgetest.runner import Context, Failed, Run
|
||||
from forgetest.suite import cloud
|
||||
|
||||
@@ -41,22 +36,6 @@ def fixture(name):
|
||||
return f.read().decode().splitlines()
|
||||
|
||||
|
||||
# What the build under test logs around a print, in the machine's own format.
|
||||
WARM_UP_LINE = ("2026-08-17T09:44:02.100000+00:00 gfcloud[1522] INFO "
|
||||
"machine:_dwell warm up: holding 3.0 s")
|
||||
COOL_DOWN_LINE = ("2026-08-17T09:45:31.700000+00:00 gfcloud[1522] INFO "
|
||||
"machine:_dwell cool down: holding 10.0 s")
|
||||
PROGRESS_LINE = ("2026-08-17T09:44:05.200000+00:00 gfcloud[1522] INFO "
|
||||
"machine:__init__ print:progress: reporting against 47848 bytes every 30 s")
|
||||
LIMITS_LINE = ("2026-08-17T09:44:01.900000+00:00 gfcloud[1522] INFO "
|
||||
"machine:_motion job limits from the header: air_assist_min_rpm=116 "
|
||||
"coolant_max_c=33.0 coolant_min_c=5.0")
|
||||
# What the engine logs when the job's limits reach it (forgectrl's log).
|
||||
EFFECTIVE_LINE = ("2026-08-17T09:44:02.050000+00:00 forgectrl[410] INFO cool: effective limits: "
|
||||
"coolant ceiling 33.0 C (local 33.0, header 33.0) resume 31.0 C; floors coolant "
|
||||
"5.0 C, exhaust 0 rpm, intake 0 rpm, air assist 116 rpm (from the header, no gate yet)")
|
||||
|
||||
|
||||
def cut(lines, marker, count=1):
|
||||
"""(before, after) at the count-th line containing marker (the line
|
||||
itself opens `after`)."""
|
||||
@@ -196,8 +175,6 @@ class CloudSuiteTests(unittest.TestCase):
|
||||
cloud.NOHUNT_MARKER = os.path.join(self.tmp, "nohunt-marker")
|
||||
FakeOffline.sent = []
|
||||
FakeOffline.on_send = None
|
||||
self.engine_line = EFFECTIVE_LINE # what the engine logs at the print; None = nothing
|
||||
self.client_limits = True # the client names its header limits
|
||||
cloud.QUIET_S = 0.4
|
||||
# A deadline, not a wait: a quiet machine is seen at once. The
|
||||
# replays land their lines from threads, so under a loaded host a
|
||||
@@ -287,635 +264,7 @@ class CloudSuiteTests(unittest.TestCase):
|
||||
self.assertIn(needle, str(cm.exception))
|
||||
return cm.exception
|
||||
|
||||
# -- session detection -------------------------------------------------
|
||||
def test_offline_mark_is_not_a_live_session(self):
|
||||
self.append(["2026-08-17T09:41:00.500000+00:00 gfcloud[3100] INFO websocket:_on_open RX-EVENT: ready",
|
||||
OFFLINE_LINE])
|
||||
live, detail = cloud.session_live(3100)
|
||||
self.assertFalse(live)
|
||||
self.assertIn("offline", detail)
|
||||
self.assertTrue(cloud.client_offline(3100))
|
||||
|
||||
# -- the emulator ------------------------------------------------------------------
|
||||
EMU_PID = 4100
|
||||
EMU_START = ["2026-08-22T23:50:00.100000+00:00 gfcloud[4100] INFO ffmachine:build_emulator EMULATE: the "
|
||||
"emulator in this machine's identity, frames from /usr/share/gfutilities/emulator, no hardware",
|
||||
"2026-08-22T23:50:02.000000+00:00 gfcloud[4100] INFO authentication:authenticate_machine SUCCESS",
|
||||
"2026-08-22T23:50:03.000000+00:00 gfcloud[4100] INFO websocket:ws_connect ESTABLISHED",
|
||||
"2026-08-22T23:50:05.000000+00:00 gfcloud[4100] INFO basemachine:_finish_action hunt [1578300001]: "
|
||||
"finished with event \":completed\"",
|
||||
"2026-08-22T23:50:06.000000+00:00 gfcloud[4100] INFO websocket:img_upload COMPLETE",
|
||||
"2026-08-22T23:50:06.500000+00:00 gfcloud[4100] INFO basemachine:_finish_action lid_image [1578300002]: "
|
||||
"finished with event \":completed\""]
|
||||
EMU_PRINT = ["2026-08-22T23:51:00.000000+00:00 gfcloud[4100] INFO gfuiservice:run service action request: print (ready)",
|
||||
"2026-08-22T23:51:01.000000+00:00 gfcloud[4100] INFO websocket:fetch_motion pulse data is uncompressed, "
|
||||
"47848 byte body",
|
||||
"2026-08-22T23:51:02.000000+00:00 gfcloud[4100] INFO basemachine:_finish_action print [1578300003]: "
|
||||
"finished with event \":completed\""]
|
||||
REAL_BACK = ["2026-08-22T23:52:01.000000+00:00 gfcloud[4200] INFO ffmachine:inhibit_connect_hunt NO-HUNT: the "
|
||||
"first settings report in the reconnect form; the service keeps its head position, no "
|
||||
"connect-time hunt",
|
||||
"2026-08-22T23:52:02.000000+00:00 gfcloud[4200] INFO authentication:authenticate_machine SUCCESS",
|
||||
"2026-08-22T23:52:03.000000+00:00 gfcloud[4200] INFO websocket:ws_connect ESTABLISHED",
|
||||
"2026-08-22T23:52:03.500000+00:00 gfcloud[4200] INFO websocket:_on_open RX-EVENT: ready"]
|
||||
|
||||
def emulator_setup(self, print_lines=None, info=True):
|
||||
self.in_cloud(pid=2278)
|
||||
self.append(["2026-08-22T23:49:00.500000+00:00 gfcloud[2278] INFO websocket:_on_open RX-EVENT: ready"])
|
||||
starts = []
|
||||
|
||||
def on_post(path, form):
|
||||
if path == "/controller/stop":
|
||||
self.fc.state["mode"] = dict(self.fc.state["mode"], controller="standby", pid=0)
|
||||
elif path == "/controller/start":
|
||||
starts.append((os.path.exists(cloud.EMULATE_MARKER), os.path.exists(cloud.NOHUNT_MARKER)))
|
||||
if len(starts) == 1:
|
||||
self.fc.state["mode"] = dict(self.fc.state["mode"], controller="running", pid=self.EMU_PID)
|
||||
self.append(self.EMU_START, delay=0.2)
|
||||
else:
|
||||
self.fc.state["mode"] = dict(self.fc.state["mode"], controller="running", pid=4200)
|
||||
self.append(self.REAL_BACK, delay=0.2)
|
||||
return None
|
||||
self.fc.on_post = on_post
|
||||
|
||||
def at_print():
|
||||
os.makedirs(cloud.EMULATOR_WORK, exist_ok=True)
|
||||
if info:
|
||||
with open(os.path.join(cloud.EMULATOR_WORK, "2026-08-22_235101_print.info"), "w") as f:
|
||||
json.dump({"header_data": {"STfr": 10000, "MCsn": 0, "PDfm": 0}}, f)
|
||||
self.append(print_lines if print_lines is not None else self.EMU_PRINT, delay=0.1)
|
||||
return starts, {"Set up any small job": at_print}
|
||||
|
||||
def test_service_protocol_against_the_emulator(self):
|
||||
starts, hooks = self.emulator_setup()
|
||||
run = self.run_test(cloud.service_protocol, hooks=hooks, test_id="cloud.service-protocol")
|
||||
ev = run.evidence
|
||||
# the emulator under its marker with the hunt; the real client back without the hunt
|
||||
self.assertEqual(starts, [(True, False), (False, True)])
|
||||
self.assertFalse(os.path.exists(cloud.EMULATE_MARKER))
|
||||
self.assertFalse(os.path.exists(cloud.NOHUNT_MARKER))
|
||||
self.assertTrue(any("no hunt" in l for l in run.lines), run.lines[-5:])
|
||||
self.assertEqual([p for p, _ in self.fc.posts],
|
||||
["/controller/stop", "/controller/start", "/controller/stop", "/controller/start"])
|
||||
self.assertIn(":completed", ev["hunt"])
|
||||
self.assertIn(":completed", ev["print"])
|
||||
self.assertEqual(ev["images_uploaded"], 1)
|
||||
self.assertEqual(ev["header_stfr"], 10000)
|
||||
self.assertEqual(self.script.asked, ["Does the app show the print complete?"])
|
||||
self.assertTrue(any("PASS: session, hunt" in l for l in run.lines), run.lines[-5:])
|
||||
self.assertFalse(os.path.exists(cloud.EMULATOR_WORK))
|
||||
|
||||
def test_service_protocol_fails_when_the_print_does_not_complete_and_still_restores_the_client(self):
|
||||
failed = [l.replace(':completed"', ':failed"') for l in self.EMU_PRINT]
|
||||
starts, hooks = self.emulator_setup(print_lines=failed)
|
||||
self.assertFails(cloud.service_protocol, "did not complete", hooks=hooks)
|
||||
self.assertEqual(starts, [(True, False), (False, True)]) # the real client came back regardless
|
||||
|
||||
def test_markers_the_client_consumed_are_not_missed(self):
|
||||
# gfcloud takes a marker it read down itself; the suite's own removal is then a no-op
|
||||
starts, hooks = self.emulator_setup()
|
||||
on_post = self.fc.on_post
|
||||
|
||||
def consuming(path, form):
|
||||
r = on_post(path, form)
|
||||
if path == "/controller/start":
|
||||
for p in (cloud.EMULATE_MARKER, cloud.NOHUNT_MARKER):
|
||||
if os.path.exists(p):
|
||||
os.remove(p)
|
||||
return r
|
||||
self.fc.on_post = consuming
|
||||
run = self.run_test(cloud.service_protocol, hooks=hooks, test_id="cloud.service-protocol")
|
||||
self.assertEqual(starts, [(True, False), (False, True)])
|
||||
self.assertTrue(any("PASS: session, hunt" in l for l in run.lines), run.lines[-5:])
|
||||
self.assertFalse(any("marker removed" in l for l in run.lines), run.lines)
|
||||
|
||||
def test_the_emulators_session_is_not_the_machines(self):
|
||||
self.append(self.EMU_START + ["2026-08-22T23:50:07.000000+00:00 gfcloud[4100] INFO websocket:_on_open "
|
||||
"RX-EVENT: ready"])
|
||||
live, detail = cloud.session_live(4100)
|
||||
self.assertFalse(live)
|
||||
self.assertIn("emulator", detail)
|
||||
|
||||
# -- the offline entry ----------------------------------------------------------
|
||||
def test_enter_offline_reuses_a_running_offline_client(self):
|
||||
self.in_offline()
|
||||
run = Run("test", "cloud.x", "cloud.x")
|
||||
run.baseline_captured = {"mode": "cloud", "position": [0, 0, 0]}
|
||||
ctx = Context(run, None, helpers.make_test("cloud.x", []))
|
||||
cloud.enter_offline(ctx)
|
||||
self.assertEqual(self.fc.posts, [])
|
||||
self.assertFalse(os.path.exists(cloud.OFFLINE_MARKER))
|
||||
|
||||
def test_enter_offline_from_grbl_sets_the_marker_for_one_start_and_declares_the_mode(self):
|
||||
seen = {}
|
||||
|
||||
def on_post(path, form):
|
||||
if path == "/mode":
|
||||
seen["marker_at_start"] = os.path.exists(cloud.OFFLINE_MARKER)
|
||||
self.append([OFFLINE_LINE], delay=0.2)
|
||||
return None
|
||||
self.fc.on_post = on_post
|
||||
run = Run("test", "cloud.x", "cloud.x")
|
||||
run.baseline_captured = {"mode": "grbl", "position": [0, 0, 0]}
|
||||
ctx = Context(run, None, helpers.make_test("cloud.x", []))
|
||||
cloud.enter_offline(ctx)
|
||||
self.assertEqual([p for p, _ in self.fc.posts], ["/mode"])
|
||||
self.assertTrue(seen["marker_at_start"]) # the client started offline
|
||||
self.assertFalse(os.path.exists(cloud.OFFLINE_MARKER)) # and the marker is gone again
|
||||
self.assertEqual(run.baseline_captured["mode"], "cloud")
|
||||
self.assertTrue(any("offline service up" in l for l in run.lines))
|
||||
|
||||
def test_enter_offline_restarts_a_service_client_in_cloud_mode(self):
|
||||
self.in_cloud(pid=2278)
|
||||
|
||||
def on_post(path, form):
|
||||
if path == "/controller/stop":
|
||||
self.fc.state["mode"] = dict(self.fc.state["mode"], controller="standby", pid=0)
|
||||
elif path == "/controller/start":
|
||||
self.fc.state["mode"] = dict(self.fc.state["mode"], controller="running", pid=3100)
|
||||
self.append([OFFLINE_LINE], delay=0.2)
|
||||
return None
|
||||
self.fc.on_post = on_post
|
||||
run = Run("test", "cloud.x", "cloud.x")
|
||||
run.baseline_captured = {"mode": "cloud", "position": [0, 0, 0]}
|
||||
ctx = Context(run, None, helpers.make_test("cloud.x", []))
|
||||
cloud.enter_offline(ctx)
|
||||
self.assertEqual([p for p, _ in self.fc.posts], ["/controller/stop", "/controller/start"])
|
||||
|
||||
def test_enter_cloud_restarts_an_offline_client_with_the_service(self):
|
||||
self.in_offline()
|
||||
with open(cloud.OFFLINE_MARKER, "w") as f:
|
||||
f.write("stale\n")
|
||||
lines = fixture("huntlid")
|
||||
pre, post = cut(lines, "gfuiservice:__init__ INITIALIZED")
|
||||
hunt_part, _close = cut(post, "_switch_event lid closed")
|
||||
|
||||
def on_post(path, form):
|
||||
if path == "/controller/stop":
|
||||
self.fc.state["mode"] = dict(self.fc.state["mode"], controller="standby", pid=0)
|
||||
elif path == "/controller/start":
|
||||
self.assertFalse(os.path.exists(cloud.OFFLINE_MARKER)) # taken down before the start
|
||||
self.fc.state["mode"] = dict(self.fc.state["mode"], controller="running", pid=2278)
|
||||
self.append(pre + hunt_part, delay=0.2)
|
||||
return None
|
||||
self.fc.on_post = on_post
|
||||
run = Run("test", "cloud.x", "cloud.x")
|
||||
run.baseline_captured = {"mode": "cloud", "position": [0, 0, 0]}
|
||||
ctx = Context(run, None, helpers.make_test("cloud.x", []))
|
||||
cloud.enter_cloud(ctx)
|
||||
self.assertEqual([p for p, _ in self.fc.posts], ["/controller/stop", "/controller/start"])
|
||||
self.assertTrue(any("offline service: restarting it with the service" in l for l in run.lines))
|
||||
|
||||
def test_offline_jobs_are_the_synthesized_square(self):
|
||||
from forgetest import puls
|
||||
run = Run("test", "cloud.x", "cloud.x")
|
||||
ctx = Context(run, None, helpers.make_test("cloud.x", []))
|
||||
path = cloud.offline_job(ctx, "t.puls", seconds=5)
|
||||
tags, payload = puls.parse(open(path, "rb").read())
|
||||
self.assertEqual(tags["MCsn"], 0)
|
||||
self.assertEqual(tags["STfr"], 10000)
|
||||
self.assertFalse(any(b & 0x10 for b in payload if not b & 0x80)) # no LASER bit anywhere
|
||||
self.assertEqual(payload[0], 0x80) # a leading power byte of zero
|
||||
self.assertEqual(run.evidence["jobs"]["t.puls"]["compressed"], False)
|
||||
|
||||
def test_session_live_is_per_client(self):
|
||||
lines = fixture("huntlid")
|
||||
# the old client (1927) closed and left; the new one (2278) is ready
|
||||
self.append(lines)
|
||||
self.assertTrue(cloud.session_live(2278)[0])
|
||||
self.assertFalse(cloud.session_live(1927)[0])
|
||||
# an unknown pid falls back to the newest lines (ready)
|
||||
self.assertTrue(cloud.session_live(9999)[0])
|
||||
# a drop after the ready line: not live until it is ready again
|
||||
self.append(["2026-08-17T10:00:00.000000+00:00 gfcloud[2278] INFO websocket:_on_close RX-EVENT: closed (1006, )",
|
||||
"2026-08-17T10:00:00.100000+00:00 gfcloud[2278] INFO websocket:run RECONNECTING"])
|
||||
live, detail = cloud.session_live(2278)
|
||||
self.assertFalse(live)
|
||||
self.assertIn("RECONNECTING", detail)
|
||||
self.append(["2026-08-17T10:00:06.000000+00:00 gfcloud[2278] INFO websocket:_on_open RX-EVENT: ready"])
|
||||
self.assertTrue(cloud.session_live(2278)[0])
|
||||
|
||||
def test_enter_cloud_reuses_a_live_session_and_never_switches(self):
|
||||
self.in_cloud()
|
||||
self.append(fixture("huntlid"))
|
||||
run = self.run_test(lambda ctx: cloud.enter_cloud(ctx))
|
||||
self.assertTrue(any("reusing it" in l for l in run.lines), run.lines)
|
||||
self.assertEqual(self.fc.posts, [])
|
||||
self.assertEqual(run.baseline_captured["mode"], "cloud")
|
||||
|
||||
NOHUNT_CLIENT = ["2026-08-22T23:52:01.000000+00:00 gfcloud[4200] INFO ffmachine:inhibit_connect_hunt NO-HUNT: the "
|
||||
"first settings report in the reconnect form; the service keeps its head position, no "
|
||||
"connect-time hunt",
|
||||
"2026-08-22T23:52:02.000000+00:00 gfcloud[4200] INFO authentication:authenticate_machine SUCCESS",
|
||||
"2026-08-22T23:52:03.000000+00:00 gfcloud[4200] INFO websocket:ws_connect ESTABLISHED",
|
||||
"2026-08-22T23:52:03.500000+00:00 gfcloud[4200] INFO websocket:_on_open RX-EVENT: ready"]
|
||||
|
||||
def test_session_hunted_is_the_clients_own_hunt(self):
|
||||
self.append(fixture("huntlid"))
|
||||
self.assertTrue(cloud.session_hunted(2278)) # its connect-time hunt completed
|
||||
self.assertFalse(cloud.session_hunted(9999)) # no lines: unknown is not hunted
|
||||
self.append(self.NOHUNT_CLIENT)
|
||||
self.assertTrue(cloud.session_live(4200)[0])
|
||||
self.assertFalse(cloud.session_hunted(4200)) # live, never hunted
|
||||
self.append(self.EMU_START)
|
||||
self.assertFalse(cloud.session_hunted(4100)) # the emulator's hunt is not the machine's
|
||||
# a later re-hunt (the lid closed) makes a no-hunt client hunted
|
||||
self.append(["2026-08-22T23:55:00.000000+00:00 gfcloud[4200] INFO basemachine:_finish_action hunt [1578300020]: "
|
||||
"finished with event \":completed\""])
|
||||
self.assertTrue(cloud.session_hunted(4200))
|
||||
|
||||
def test_enter_cloud_restarts_a_live_session_that_never_hunted_with_the_hunt(self):
|
||||
# the real print must not ride a head position the service only believes
|
||||
self.in_cloud(pid=4200)
|
||||
self.append(fixture("huntlid") + self.NOHUNT_CLIENT)
|
||||
lines = fixture("huntlid")
|
||||
_, post = cut(lines, "gfuiservice:__init__ INITIALIZED")
|
||||
starts = []
|
||||
|
||||
def on_post(path, form):
|
||||
if path == "/controller/stop":
|
||||
self.fc.state["mode"] = dict(self.fc.state["mode"], controller="standby", pid=0)
|
||||
elif path == "/controller/start":
|
||||
starts.append(os.path.exists(cloud.NOHUNT_MARKER))
|
||||
self.fc.state["mode"] = dict(self.fc.state["mode"], controller="running", pid=2278)
|
||||
self.append(post, delay=0.2)
|
||||
return None
|
||||
self.fc.on_post = on_post
|
||||
run = self.run_test(lambda ctx: cloud.enter_cloud(ctx))
|
||||
self.assertEqual([p for p, _ in self.fc.posts], ["/controller/stop", "/controller/start"])
|
||||
self.assertEqual(starts, [False]) # with the hunt: no marker
|
||||
self.assertTrue(any("never hunted" in l for l in run.lines), run.lines)
|
||||
self.assertTrue(any("connect-time hunt" in l and ":completed" in l for l in run.lines), run.lines)
|
||||
|
||||
def test_enter_cloud_refuses_a_dead_session(self):
|
||||
self.in_cloud(pid=1927) # the client that shut down in the excerpt
|
||||
self.append(fixture("huntlid"))
|
||||
self.assertFails(cloud.enter_cloud, "no live service session")
|
||||
|
||||
def test_enter_cloud_switches_once_from_grbl_and_declares_it(self):
|
||||
lines = fixture("huntlid")
|
||||
pre, post = cut(lines, "gfuiservice:__init__ INITIALIZED")
|
||||
self.append(pre)
|
||||
|
||||
def on_post(path, form):
|
||||
if path == "/mode":
|
||||
self.append(post, delay=0.2) # the new client's session + hunt + moves
|
||||
return None
|
||||
self.fc.on_post = on_post
|
||||
run = self.run_test(lambda ctx: cloud.enter_cloud(ctx))
|
||||
self.assertEqual([p for p, _ in self.fc.posts], ["/mode"])
|
||||
self.assertEqual(self.fc.state["mode"]["mode"], "cloud")
|
||||
self.assertEqual(run.baseline_captured["mode"], "cloud") # declared: the baseline keeps it
|
||||
self.assertTrue(any("connect-time hunt" in l and ":completed" in l for l in run.lines), run.lines)
|
||||
|
||||
def test_enter_cloud_waits_for_the_service_to_stop_moving(self):
|
||||
self.in_cloud()
|
||||
self.append(fixture("huntlid"))
|
||||
# a motion in flight (never idle within the timeout) fails, and says so
|
||||
self.fc.state["status"]["state"] = "running"
|
||||
cloud.QUIET_TIMEOUT_S = 3 # this one waits the deadline out; tearDown puts it back
|
||||
self.assertFails(cloud.enter_cloud, "still running service moves")
|
||||
|
||||
def test_the_quiet_waits_out_the_service_thinking_after_a_lid_image(self):
|
||||
# Between a lid image and the service's next move the log is silent
|
||||
# while the service works on the image (the re-hunt on the bench
|
||||
# reference, 2026-09-25). The quiet counts from the image's own
|
||||
# lines, so it is not declared in that gap: it comes after the move.
|
||||
cloud.QUIET_S = 1.0
|
||||
offset = cloud.log_size(self.log)
|
||||
p = "2026-09-25T16:43:%s+00:00 gfcloud[12821] INFO "
|
||||
self.append([p % "20.103479" + "gfuiservice:run service action request: motion (ready)",
|
||||
p % "20.105376" + "machine:_motion start motion",
|
||||
p % "22.617720" + "machine:_motion_locked end positions (12120, 7402, 0)",
|
||||
p % "22.618542" + "machine:_motion end motion",
|
||||
p % "22.619218" + "basemachine:_finish_action motion [1588864337]: finished with event "
|
||||
"\":completed\""])
|
||||
self.append([p % "23.249589" + "gfuiservice:run service action request: lid_image (ready)",
|
||||
p % "24.686536" + "websocket:img_upload COMPLETE"], delay=0.8)
|
||||
self.append([p % "29.667978" + "gfuiservice:run service action request: motion (ready)",
|
||||
p % "29.671663" + "machine:_motion start motion"], delay=1.6)
|
||||
run = Run("test", "cloud.x", "cloud.x")
|
||||
ctx = Context(run, None, helpers.make_test("cloud.x", []))
|
||||
self.assertTrue(cloud.wait_quiet(ctx, offset))
|
||||
self.assertIn("service action request: motion", cloud.log_lines_since(self.log, offset)[-2])
|
||||
|
||||
# -- the hunt with the lid open ------------------------------------------
|
||||
# -- the mode switch: hunt with the lid open, then $H -----------------------
|
||||
HUNT_RUN_SAMPLE = {"phase": "run", "verdict": "ok", "armed": False,
|
||||
"fan_gates": {"exhaust": {"state": "unjudged", "reading": 0, "floor": 500}}}
|
||||
# A homing session's motions as gfhome logs them (the bench reference,
|
||||
# 2026-09-25): a small correction, then into the camera home.
|
||||
HOME_SESSION = [
|
||||
"2026-09-25T16:43:45.374951+00:00 gfhome[12982] INFO machine:_motion start motion",
|
||||
"2026-09-25T16:43:45.853976+00:00 gfhome[12982] INFO machine:_motion_locked end positions (-13, -13, 0)",
|
||||
"2026-09-25T16:43:45.854819+00:00 gfhome[12982] INFO machine:_motion end motion",
|
||||
"2026-09-25T16:43:45.856076+00:00 gfhome[12982] INFO basemachine:_finish_action motion [1588864529]: "
|
||||
"finished with event \":completed\"",
|
||||
"2026-09-25T16:43:51.600734+00:00 gfhome[12982] INFO machine:_motion start motion",
|
||||
"2026-09-25T16:43:53.605402+00:00 gfhome[12982] INFO machine:_motion_locked end positions (-13095, -7399, 0)",
|
||||
"2026-09-25T16:43:53.606171+00:00 gfhome[12982] INFO machine:_motion end motion",
|
||||
"2026-09-25T16:43:53.606916+00:00 gfhome[12982] INFO basemachine:_finish_action motion [1588864567]: "
|
||||
"finished with event \":completed\""]
|
||||
|
||||
def mode_switch_setup(self, hunt_lines=None, home_complete=True, lid_late=False, home_lines=None):
|
||||
"""The fakes a mode-switch run needs: grbl to answer $H, the lid
|
||||
lamp attr, homing_mode = gfcloud, the service lines landing on
|
||||
the switch to cloud - the client's start at once, its session and
|
||||
hunt once the lid reads open, as on the bench where the lid opens
|
||||
behind the controller's start and ahead of the hunt (the hunt
|
||||
reads as a run to the cooling engine while it lasts) - the re-hunt
|
||||
on the lid close, and gfhome finishing the homing after $H. With
|
||||
lid_late the hunt is requested before the lid opens: the race the
|
||||
test must call. The step counters are the machine's: a controller
|
||||
start zeroes them and removes the homing anchor, the home zeroes
|
||||
them and writes it, and a jog moves them by what it asked."""
|
||||
self.grbl = helpers.FakeGrbl().start()
|
||||
os.makedirs(self.sysfs + "pic", exist_ok=True)
|
||||
self._attr("pic/lid_led", "236")
|
||||
self.fc.state["settings"]["homing_mode"] = "gfcloud"
|
||||
anchor = os.path.join(self.tmp, "grblhal.homed")
|
||||
self.addCleanup(setattr, baseline, "ANCHOR_PATH", baseline.ANCHOR_PATH)
|
||||
baseline.ANCHOR_PATH = anchor
|
||||
lines = fixture("huntlid")
|
||||
pre, post = cut(lines, "gfuiservice:__init__ INITIALIZED")
|
||||
hunt_part, close_part = cut(post, "_switch_event lid closed")
|
||||
if hunt_lines is not None:
|
||||
hunt_part = hunt_lines(hunt_part)
|
||||
fc = self.fc
|
||||
|
||||
def controller_start():
|
||||
self._pos(0, 0, 3)
|
||||
if os.path.exists(anchor):
|
||||
os.remove(anchor)
|
||||
|
||||
def jog(line):
|
||||
if line.startswith("$J=G91"):
|
||||
words = dict((w[0], float(w[1:])) for w in line[3:].split() if w[0] in "XY")
|
||||
with open(self.sysfs + "cnc/position", "rb") as f:
|
||||
x, y, z = struct.unpack("<3i", f.read(12))
|
||||
self._pos(x + round(words.get("X", 0.0) * baseline.XY_STEPS_PER_MM),
|
||||
y + round(words.get("Y", 0.0) * baseline.XY_STEPS_PER_MM), z)
|
||||
self.grbl.on_command = jog
|
||||
|
||||
def hunt():
|
||||
self.append(hunt_part, delay=0.0)
|
||||
time.sleep(4.0) # the hunt outlasts the session wait on the bench
|
||||
fc.state["cool"] = {"phase": "idle", "armed": False, "hold": False}
|
||||
|
||||
def on_post(path, form):
|
||||
if path == "/mode" and form.get("controller") == "cloud":
|
||||
fc.state["cool"] = dict(self.HUNT_RUN_SAMPLE)
|
||||
|
||||
def land():
|
||||
self.append(pre, delay=0.0)
|
||||
if lid_late:
|
||||
return
|
||||
t0 = time.time()
|
||||
while fc.state["status"]["switches"]["lid"] and time.time() - t0 < 5:
|
||||
time.sleep(0.02)
|
||||
time.sleep(0.3)
|
||||
hunt()
|
||||
threading.Thread(target=land, daemon=True).start()
|
||||
elif path == "/mode" and form.get("controller") == "grbl":
|
||||
controller_start()
|
||||
self.grbl.state = "Idle"
|
||||
elif path == "/controller/stop":
|
||||
fc.state["mode"] = dict(fc.state["mode"], controller="standby", pid=0)
|
||||
elif path == "/controller/start":
|
||||
controller_start()
|
||||
fc.state["mode"] = dict(fc.state["mode"], controller="running", pid=4300, motion="verified")
|
||||
fc.state["status"]["homed"] = False
|
||||
return None
|
||||
self.fc.on_post = on_post
|
||||
|
||||
def lid_opened():
|
||||
if lid_late:
|
||||
self.append(hunt_part, delay=0.0)
|
||||
self.lid(False)
|
||||
|
||||
def lid_closed():
|
||||
self.lid(True)
|
||||
self.append(close_part, delay=0.2)
|
||||
|
||||
def homing():
|
||||
self.grbl.state = "Home"
|
||||
with open(self.homelog, "ab") as f:
|
||||
f.write(("\n".join(home_lines or self.HOME_SESSION) + "\n").encode())
|
||||
time.sleep(0.4)
|
||||
self._pos(0, 0, 3)
|
||||
open(anchor, "w").close()
|
||||
self.grbl.state = "Idle"
|
||||
self.fc.state["status"]["homed"] = True
|
||||
if home_complete:
|
||||
with open(self.homelog, "ab") as f:
|
||||
f.write(b"2026-08-17T09:46:00.000000+00:00 gfhome[2300] INFO homing complete "
|
||||
b"(service quiet 8s, 3 motion windows)\n")
|
||||
self.home_hook = homing
|
||||
return {"Open the lid.": lid_opened, "Close the lid.": lid_closed}
|
||||
|
||||
def wait_home_command(self):
|
||||
while "$H" not in self.grbl.sent:
|
||||
time.sleep(0.02)
|
||||
self.home_hook()
|
||||
|
||||
def test_mode_switch_round_trip_with_the_hunt_lid_open_and_the_homing(self):
|
||||
hooks = self.mode_switch_setup()
|
||||
threading.Thread(target=self.wait_home_command, daemon=True).start()
|
||||
run = self.run_test(cloud.mode_switch, hooks=hooks, test_id="cloud.mode-switch")
|
||||
ev = run.evidence
|
||||
posts = [(p, f.get("controller")) for p, f in self.fc.posts if p == "/mode"]
|
||||
self.assertEqual(posts, [("/mode", "cloud"), ("/mode", "grbl")])
|
||||
self.assertEqual(self.fc.state["mode"]["mode"], "grbl")
|
||||
self.assertIn(":completed", ev["hunt_line"])
|
||||
self.assertEqual(ev["refusals_before_hunt_end"], 0)
|
||||
self.assertTrue(ev["lens_homed"])
|
||||
self.assertEqual(ev["motions_after_lid_close"], 3)
|
||||
self.assertTrue(all(c.get("verdict") != "AIRFLOW" for c in ev["hunt_gates"]))
|
||||
self.assertTrue(ev["homed"])
|
||||
self.assertIn("3 motion windows", ev["gfhome_complete"])
|
||||
self.assertIn("$H", self.grbl.sent)
|
||||
# the lid was a machine action, not a prompt: two notices, no questions
|
||||
self.assertEqual([r["state"] for r in ev["actions"]], ["open", "close"])
|
||||
self.assertEqual(self.script.asked, [])
|
||||
self.assertTrue(any("PASS:" in l for l in run.lines), run.lines[-5:])
|
||||
# The head goes back where the test found it, each stretch by its
|
||||
# client's record: the re-hunt of the excerpt ends 2/7 steps from
|
||||
# where the cloud mode began, and the homing went into the corner.
|
||||
self.assertEqual(tuple(ev["cloud_return"]["session_travel_steps"]), (2, 7))
|
||||
self.assertEqual(tuple(ev["return"]["session_travel_steps"]), (-13108, -7412))
|
||||
spm = baseline.XY_STEPS_PER_MM
|
||||
self.assertEqual([l for l in self.grbl.sent if l.startswith("$J=")],
|
||||
["$J=G91 G21 X%.3f Y%.3f F2400" % (-2 / spm, -7 / spm),
|
||||
"$J=G91 G21 X%.3f Y%.3f F2400" % (13108 / spm, 7412 / spm)])
|
||||
# a start after each jog, and the camera home dropped before the second
|
||||
self.assertEqual([p for p, f in self.fc.posts if p.startswith("/controller/")],
|
||||
["/controller/stop", "/controller/start"] * 3)
|
||||
with open(self.sysfs + "cnc/position", "rb") as f:
|
||||
self.assertEqual(struct.unpack("<3i", f.read(12))[:2], (0, 0))
|
||||
|
||||
def test_mode_switch_a_hand_back_that_fails_too_does_not_hide_the_failure(self):
|
||||
# gfhome never said it homed, and its last motion has no end on
|
||||
# record: the travel is unknown, the head is not moved, and the
|
||||
# failure reported is the test's own
|
||||
hooks = self.mode_switch_setup(home_complete=False, home_lines=self.HOME_SESSION[:5])
|
||||
threading.Thread(target=self.wait_home_command, daemon=True).start()
|
||||
self.assertFails(cloud.mode_switch, "no 'homing complete' line", hooks=hooks)
|
||||
run = self.script.run
|
||||
self.assertTrue(any("the head is not back" in l for l in run.lines), run.lines[-6:])
|
||||
self.assertIn("cannot be known", run.evidence["return"]["failed"])
|
||||
self.assertEqual(len([l for l in self.grbl.sent if l.startswith("$J=")]), 1) # the cloud stretch's
|
||||
|
||||
def test_mode_switch_fails_when_the_travel_in_cloud_mode_cannot_be_known(self):
|
||||
# the hunt's motion has no end on record (a client killed inside it)
|
||||
hooks = self.mode_switch_setup(
|
||||
hunt_lines=lambda part: [l for l in part if not l.endswith("machine:_motion end motion")])
|
||||
self.assertFails(cloud.mode_switch, "the travel in cloud mode cannot be known", hooks=hooks)
|
||||
self.assertEqual([l for l in self.grbl.sent if l.startswith("$J=")], [])
|
||||
|
||||
def test_mode_switch_opens_the_lid_behind_the_controller_and_ahead_of_the_hunt(self):
|
||||
# No controller starts with the enclosure open: the switch is made
|
||||
# lid closed, the lid opens once the controller is up, and the
|
||||
# hunt's request lands after that - the order the test records.
|
||||
hooks = self.mode_switch_setup()
|
||||
threading.Thread(target=self.wait_home_command, daemon=True).start()
|
||||
run = self.run_test(cloud.mode_switch, hooks=hooks, test_id="cloud.mode-switch")
|
||||
self.assertFalse(run.evidence["hunt_before_lid_open"])
|
||||
acts = [(a["channel"], a["state"]) for a in run.evidence["actions"]]
|
||||
self.assertEqual(acts, [("lid", "open"), ("lid", "close")])
|
||||
i_post = next(i for i, l in enumerate(run.lines) if "POST /mode controller=cloud" in l)
|
||||
i_open = next(i for i, l in enumerate(run.lines) if "ACT lid open" in l)
|
||||
self.assertLess(i_post, i_open)
|
||||
|
||||
def test_mode_switch_refuses_to_start_with_the_lid_open(self):
|
||||
hooks = self.mode_switch_setup()
|
||||
self.lid(False)
|
||||
self.assertFails(cloud.mode_switch, "close the lid first", hooks=hooks)
|
||||
|
||||
def test_mode_switch_fails_when_the_hunt_was_requested_before_the_lid_opened(self):
|
||||
hooks = self.mode_switch_setup(lid_late=True)
|
||||
self.assertFails(cloud.mode_switch, "before the lid was open", hooks=hooks)
|
||||
|
||||
def test_mode_switch_fails_when_the_hunt_is_refused_for_the_lid(self):
|
||||
def refused(hunt_part):
|
||||
i = next(i for i, l in enumerate(hunt_part) if "z_axis:home starting z homing cycle" in l)
|
||||
return hunt_part[:i] + ["2026-08-17T09:45:10.595000+00:00 gfcloud[2278] INFO machine:_safe_to_move "
|
||||
"lid opened, unsafe to move"] + hunt_part[i:]
|
||||
hooks = self.mode_switch_setup(hunt_lines=refused)
|
||||
self.assertFails(cloud.mode_switch, "refused for the lid", hooks=hooks)
|
||||
|
||||
def test_mode_switch_fails_when_the_hunt_skipped_the_lens(self):
|
||||
hooks = self.mode_switch_setup(hunt_lines=lambda part: [l for l in part if "z homing cycle" not in l])
|
||||
self.assertFails(cloud.mode_switch, "did not home the lens", hooks=hooks)
|
||||
|
||||
def test_mode_switch_fails_when_gfhome_never_saw_the_head_move(self):
|
||||
hooks = self.mode_switch_setup(home_complete=False)
|
||||
threading.Thread(target=self.wait_home_command, daemon=True).start()
|
||||
self.assertFails(cloud.mode_switch, "no 'homing complete' line", hooks=hooks)
|
||||
|
||||
def test_mode_switch_needs_no_setting_from_the_operator(self):
|
||||
# The test sets the gfcloud homing itself and restores it: no precheck
|
||||
# names a setting the operator should change first.
|
||||
from forgetest import catalog
|
||||
t = catalog.load_suite()["cloud.mode-switch"]
|
||||
self.assertIsNone(getattr(t, "precheck", None))
|
||||
self.assertNotIn("homing_mode_is_gfcloud", dir(cloud))
|
||||
|
||||
# -- pause / resume ---------------------------------------------------------
|
||||
def replay_print(self, name, at_run, at_end, tail_delay=0.3):
|
||||
"""Split a print excerpt into: up to the run's RUNNING line
|
||||
(lands when the operator says Print is done), the block landing at
|
||||
the mid-run prompt, and the rest."""
|
||||
lines = fixture(name)
|
||||
pre, rest = cut(lines, "waiting for button")
|
||||
run_pre, rest = cut(rest, "current state: MachineState.RUNNING") # the PRINT's run
|
||||
# The excerpt was captured before the machine held for a warm-up and
|
||||
# a rest, and before it reported a print's progress; the replay
|
||||
# carries those lines where it emits them now, rather than editing
|
||||
# what the machine actually said that day.
|
||||
run_pre = run_pre + ([LIMITS_LINE] if self.client_limits else []) + [WARM_UP_LINE, PROGRESS_LINE]
|
||||
pre, rest = pre + run_pre + [rest[0]], rest[1:]
|
||||
mid, tail = cut(rest, at_end)
|
||||
tail = tail + [COOL_DOWN_LINE]
|
||||
|
||||
def at_done():
|
||||
self.append(pre, delay=0.1)
|
||||
if self.engine_line:
|
||||
with open(self.fclog, "ab") as f:
|
||||
f.write((self.engine_line + "\n").encode())
|
||||
return {"Click Done here": at_done,
|
||||
at_run: lambda: (self.append(mid, delay=0.05), self.append(tail, delay=tail_delay))}
|
||||
|
||||
def test_pause_resume_passes_on_the_machines_lines(self):
|
||||
self.in_cloud(pid=1522)
|
||||
self.hunted(1522)
|
||||
self.append(["2026-08-17T09:41:00.000000+00:00 gfcloud[1522] INFO authentication:authenticate_machine SUCCESS",
|
||||
"2026-08-17T09:41:00.500000+00:00 gfcloud[1522] INFO websocket:_on_open RX-EVENT: ready",
|
||||
"2026-08-17T09:41:00.900000+00:00 gfcloud[1522] INFO websocket:ws_connect ESTABLISHED"])
|
||||
hooks = self.replay_print("pause", "the press pauses the print", "current state: MachineState.IDLE")
|
||||
self.fc.state["cool"]["armed"] = True
|
||||
run = self.run_test(cloud.pause_resume, hooks=hooks, test_id="cloud.pause-resume")
|
||||
ev = run.evidence
|
||||
self.assertEqual(ev["log"], {"button pressed mid-run; pausing": True, "paused at": True,
|
||||
"button pressed while paused": True, "resuming (laser lead": True})
|
||||
self.assertTrue(ev["armed_after_resume"])
|
||||
self.assertIn(":completed", ev["log_end"]["print finished"])
|
||||
self.assertTrue(ev["log_end"]["return home complete"])
|
||||
self.assertEqual(ev["relock_or_cancel_lines"], 0)
|
||||
self.assertEqual(self.fc.posts, [])
|
||||
self.assertTrue(any("PASS: button pause/resume" in l for l in run.lines))
|
||||
# the post-print hunt was waited out
|
||||
self.assertTrue(any("machine is quiet" in l for l in run.lines))
|
||||
# the job's envelope passed through: the client's line and the engine's
|
||||
self.assertEqual(ev["header_limits"],
|
||||
"air_assist_min_rpm=116 coolant_max_c=33.0 coolant_min_c=5.0")
|
||||
self.assertIn("header 33.0", ev["effective_limits"][0]) # the line with the header
|
||||
self.assertTrue(all("effective limits: coolant ceiling 33.0 C" in ln for ln in ev["effective_limits"]))
|
||||
|
||||
def test_pause_resume_fails_when_the_client_names_no_header_limits(self):
|
||||
self.client_limits = False
|
||||
self.in_cloud(pid=1522)
|
||||
self.hunted(1522)
|
||||
self.append(["2026-08-17T09:41:00.500000+00:00 gfcloud[1522] INFO websocket:_on_open RX-EVENT: ready"])
|
||||
hooks = self.replay_print("pause", "the press pauses the print", "current state: MachineState.IDLE")
|
||||
self.fc.state["cool"]["armed"] = True
|
||||
self.assertFails(cloud.pause_resume, "named no job limits", hooks=hooks)
|
||||
|
||||
def test_pause_resume_fails_when_the_engine_resolves_against_no_header(self):
|
||||
self.engine_line = EFFECTIVE_LINE.replace("header 33.0", "header none 0.0")
|
||||
self.in_cloud(pid=1522)
|
||||
self.hunted(1522)
|
||||
self.append(["2026-08-17T09:41:00.500000+00:00 gfcloud[1522] INFO websocket:_on_open RX-EVENT: ready"])
|
||||
hooks = self.replay_print("pause", "the press pauses the print", "current state: MachineState.IDLE")
|
||||
self.fc.state["cool"]["armed"] = True
|
||||
self.assertFails(cloud.pause_resume, "never resolved an effective ceiling", hooks=hooks)
|
||||
|
||||
def test_pause_resume_fails_when_the_print_is_cancelled_instead(self):
|
||||
self.in_cloud(pid=1522)
|
||||
self.hunted(1522)
|
||||
self.append(["2026-08-17T09:41:00.500000+00:00 gfcloud[1522] INFO websocket:_on_open RX-EVENT: ready"])
|
||||
lines = fixture("pause")
|
||||
lines = [l.replace('print [1576550507]: finished with event ":completed"',
|
||||
'print [1576550507]: finished with event ":cancelled"') for l in lines]
|
||||
pre, rest = cut(lines, "current state: MachineState.RUNNING")
|
||||
pre, rest = pre + [rest[0]], rest[1:]
|
||||
hooks = {"Click Done here": lambda: self.append(pre, delay=0.1),
|
||||
"the press pauses the print": lambda: self.append(rest, delay=0.05)}
|
||||
self.assertFails(cloud.pause_resume, "did not complete after the resume", hooks=hooks)
|
||||
|
||||
def test_pause_resume_fails_without_the_retraced_restart(self):
|
||||
"""The second press was seen but the retraced restart never logged
|
||||
(the app's resume is its own line now): the failure names that."""
|
||||
self.in_cloud(pid=1522)
|
||||
self.hunted(1522)
|
||||
self.append(["2026-08-17T09:41:00.500000+00:00 gfcloud[1522] INFO websocket:_on_open RX-EVENT: ready"])
|
||||
lines = [l for l in fixture("pause") if "resuming (laser lead" not in l]
|
||||
pre, rest = cut(lines, "current state: MachineState.RUNNING")
|
||||
pre, rest = pre + [rest[0]], rest[1:]
|
||||
with self.fast_wait_log():
|
||||
hooks = {"Click Done here": lambda: self.append(pre, delay=0.1),
|
||||
"the press pauses the print": lambda: self.append(rest, delay=0.05)}
|
||||
self.assertFails(cloud.pause_resume, "no retraced restart logged", hooks=hooks)
|
||||
|
||||
def test_pause_resume_fails_when_the_kernel_refuses_the_resume(self):
|
||||
self.in_cloud(pid=1522)
|
||||
self.hunted(1522)
|
||||
@@ -947,19 +296,6 @@ class CloudSuiteTests(unittest.TestCase):
|
||||
cloud.wait_log = saved
|
||||
cloud.PRESS_TIMEOUT_S = press
|
||||
|
||||
def test_pause_resume_fails_without_the_pause_line(self):
|
||||
self.in_cloud(pid=1522)
|
||||
self.hunted(1522)
|
||||
self.append(["2026-08-17T09:41:00.500000+00:00 gfcloud[1522] INFO websocket:_on_open RX-EVENT: ready"])
|
||||
lines = [l for l in fixture("pause") if "button pressed mid-run" not in l]
|
||||
pre, rest = cut(lines, "current state: MachineState.RUNNING")
|
||||
pre, rest = pre + [rest[0]], rest[1:]
|
||||
with self.fast_wait_log():
|
||||
hooks = {"Click Done here": lambda: self.append(pre, delay=0.1),
|
||||
"the press pauses the print": lambda: self.append(rest, delay=0.05)}
|
||||
self.assertFails(cloud.pause_resume, "did not pause the run", hooks=hooks)
|
||||
|
||||
# -- the lid/interlock abort and the button-wait tests, on their excerpts ------
|
||||
# -- the merged lid + interlock abort test -------------------------------
|
||||
def abort_parts(self):
|
||||
"""The lid-abort excerpt split for the merged test: the print
|
||||
@@ -996,54 +332,6 @@ class CloudSuiteTests(unittest.TestCase):
|
||||
"on its way back": lambda: (self.lid(False), self.append(ilk_park, delay=0.05)),
|
||||
"Restore the remote-interlock loop": restore}
|
||||
|
||||
def test_lid_interlock_abort_on_the_bench_excerpt(self):
|
||||
self.in_offline()
|
||||
prints = []
|
||||
run = self.run_test(cloud.lid_interlock_abort,
|
||||
hooks=self.abort_hooks(*self.abort_parts(), prints),
|
||||
test_id="cloud.lid-interlock-abort")
|
||||
ev = run.evidence
|
||||
self.assertEqual(prints, [9001, 9002]) # two prints, handed over the socket
|
||||
self.assertTrue(FakeOffline.sent[0]["motion_url"].endswith("abort.puls"))
|
||||
self.assertEqual(ev["jobs"]["abort.puls"]["compressed"], False)
|
||||
# print 1: the lid
|
||||
self.assertLess(ev["edge_to_stop_ms"], 60)
|
||||
self.assertIn(":cancelled", ev["lid_log"]["print finished"])
|
||||
self.assertEqual(ev["lid_counters_after_park"], [0, 0, 3])
|
||||
self.assertTrue(ev["lid_latch_locked"])
|
||||
self.assertFalse(ev["lid_armed_after"])
|
||||
# print 2: the interlock, with the lid opened during the park
|
||||
self.assertFalse(ev["interlock_ok_after_pull"])
|
||||
self.assertIn(":cancelled", ev["interlock_log"]["print finished"])
|
||||
self.assertTrue(ev["interlock_log"]["return home complete"])
|
||||
self.assertEqual(ev["switches_at_return"], {"lid": False, "interlock_ok": False})
|
||||
self.assertEqual(ev["interlock_counters_after_park"], [0, 0, 3])
|
||||
self.assertTrue(ev["interlock_latch_locked"])
|
||||
self.assertEqual(ev["restored"], {"lid": True, "interlock_ok": True})
|
||||
self.assertEqual(self.fc.posts, [])
|
||||
self.assertTrue(any("PASS: lid open" in l for l in run.lines), run.lines)
|
||||
|
||||
def test_lid_interlock_abort_refuses_when_the_loop_is_already_open(self):
|
||||
self.in_offline()
|
||||
self.fc.state["status"]["switches"]["interlock_ok"] = False
|
||||
self.assertFails(cloud.lid_interlock_abort, "already reads open")
|
||||
|
||||
def test_lid_interlock_abort_fails_when_the_park_stops_at_the_lid(self):
|
||||
# the regression this guards: a park an open lid can interrupt
|
||||
self.in_offline()
|
||||
pre, lid_tail, ilk_stop, ilk_park = self.abort_parts()
|
||||
ilk_park = [l for l in ilk_park if "return home complete" not in l]
|
||||
saved = cloud.wait_log
|
||||
|
||||
def fast_wait_log(ctx, offset, needles, timeout, poll=0.5):
|
||||
return saved(ctx, offset, needles, min(timeout, 1.5), poll=0.1)
|
||||
cloud.wait_log = fast_wait_log
|
||||
try:
|
||||
self.assertFails(cloud.lid_interlock_abort, "did not run to completion",
|
||||
hooks=self.abort_hooks(pre, lid_tail, ilk_stop, ilk_park, []))
|
||||
finally:
|
||||
cloud.wait_log = saved
|
||||
|
||||
def test_lid_interlock_abort_fails_when_the_lid_stop_is_not_edge_driven(self):
|
||||
# a polled stop (the pre-parity behavior) shows up as a long edge->stop gap
|
||||
self.in_offline()
|
||||
@@ -1072,17 +360,6 @@ class CloudSuiteTests(unittest.TestCase):
|
||||
hooks = {"press it. The print runs dark": lambda: self.append(run, delay=0.05)}
|
||||
self.assertFails(dark_print, "the latch is unlocked at the button wait", hooks=hooks)
|
||||
|
||||
def test_print_finish_is_the_prints_not_another_actions(self):
|
||||
# a motion that completes before the print must not satisfy the print's finish
|
||||
lines = fixture("lidabort")
|
||||
i = cloud.action_finish_index(lines, "print")
|
||||
j = cloud.action_finish_index(lines, "motion")
|
||||
self.assertIsNotNone(i)
|
||||
self.assertIsNotNone(j)
|
||||
self.assertLess(j, i)
|
||||
self.assertIn(":cancelled", lines[i])
|
||||
self.assertIn(":completed", lines[j])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user