From 8e1fe5090d474d77f2acf79c2772586bfd7f686b Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Tue, 13 Oct 2026 21:20:31 -0400 Subject: [PATCH] Harnesses: the null-sink cases other tests prove leave The grblHAL harnesses keep what only a running controller shows; the cases a host unit test in grblHAL-glowforge proves, and the log and reply wording, leave. - laser_lifecycle_test: test_arm_waits_for_the_fans goes. laser_arm_test case A4 (grblHAL-glowforge) proves the fan grace stretching the arm's acknowledgment budget, on the same arm path. The docstring names it. - xy_mode_test: case_x32, case_invalid and the log checks go; the default, x16 and the low tick stay. xy_scale_test holds each mode's tick and the not-a-mode parse. - planner_blocks_test: one restart, at $398 1000; the 400 pass and the fresh-store probe go. - ctlport_test: test_poll_lf and test_crlf go (the CR LF poll stays); in test_sender_out the network turn-away goes (serial_test holds it), and the busy gating, the grant and the outlives-client part stay. test_port_dark is whole. - mcode_test: the timeout case runs on the host build's 5 s GFMCODE_WAIT_S (grblHAL-glowforge's CI configures it; the image keeps 30 s) and still shows the hold, at 5.1 s; 5 s outlasts the 3 s test_dark watches the wait; test_table refuses one form per parser branch. test_timeout_holds and test_dark stay. - envelope_test: test_unset and test_bounds go (lens_home_test holds the clamp); test_armed and the open, apply and close cases stay. - manual_home_test: the out-of-range offset block goes, a repeat of gfhome_clamp_home_mm (lens_home_test); the modal-M3 FIRE check and every release and energize case stay. - cool_report_test: one non-hex secret and one CR LF injection stay; runner-malformed and the exact header-set checks go. A bad secret is never sent and a good one is. - tray_test: test_offset keeps 20 mm; test_errors keeps a bad P and a bad axis; the frame shift, envelope, barrier, persistence, port op, unreferenced lens and camera home stay. Proof: with grblHAL-glowforge's matching commit, the eleven harnesses its CI runs pass under Linux against the null-sink build: nine on a build with a 1 s M-code wait, and mcode_test and tray_test, the two that wait at an M-code, on the 5 s build CI configures (-DGFMCODE_WAIT_S=5.0). pyflakes reports nothing in the nine changed files. Push this before grblHAL-glowforge's commit. Acceptance: none; host harnesses only. --- scripts/bench/cool_report_test.py | 22 +++------ scripts/bench/ctlport_test.py | 71 ++++----------------------- scripts/bench/envelope_test.py | 35 +++---------- scripts/bench/laser_lifecycle_test.py | 29 +---------- scripts/bench/manual_home_test.py | 18 ++----- scripts/bench/mcode_test.py | 20 +++++--- scripts/bench/planner_blocks_test.py | 22 ++------- scripts/bench/tray_test.py | 16 +++--- scripts/bench/xy_mode_test.py | 49 ++++-------------- 9 files changed, 66 insertions(+), 216 deletions(-) diff --git a/scripts/bench/cool_report_test.py b/scripts/bench/cool_report_test.py index ca920e5..188fae9 100644 --- a/scripts/bench/cool_report_test.py +++ b/scripts/bench/cool_report_test.py @@ -11,16 +11,16 @@ the route asks for it. This harness stands in for forgectrl's listener, runs the null-sink controller against it, and reads every report as it arrives. secret every report carries X-ForgeFIRM-Report with the secret it was - started with, and the request is otherwise as it always was + started with none started with no secret, the reports carry no such header - malformed a value that is not 32 hex digits (short, a letter out of range, - a CR LF and a header of its own behind it) is never sent, and - nothing of it reaches the wire + malformed a value that is not 32 hex digits (a letter out of range, or a + CR LF and a header of its own behind it, 32 characters long) is + never sent, and nothing of it reaches the wire runner the homing runner the controller starts is handed the secret (it reports in the controller's place while a gfcloud $H holds the machine, and the controller is quiet then), and the reports - still carry it afterwards; a controller with no secret, or with - one that is not 32 hex digits, hands the runner none + still carry it afterwards; a controller with no secret hands the + runner none Usage: cool_report_test.py """ @@ -170,15 +170,11 @@ def run(name, secret, check, runner=False, handed=None): def with_secret(name, r): if r["headers"].get("x-forgefirm-report") != SECRET: fail("[%s] a report without the secret: %s" % (name, r["headers"])) - if set(r["headers"]) != {"host", "connection", "content-length", "x-forgefirm-report"}: - fail("[%s] the report's headers changed: %s" % (name, sorted(r["headers"]))) def without(name, r): if "x-forgefirm-report" in r["headers"] or "x-evil" in r["headers"] or b"Evil" in r["raw"]: fail("[%s] a header that must not be sent: %s" % (name, r["headers"])) - if set(r["headers"]) != {"host", "connection", "content-length"}: - fail("[%s] the report's headers changed: %s" % (name, sorted(r["headers"]))) def main(): @@ -186,13 +182,11 @@ def main(): fail("no controller binary at %s" % BIN) run("secret", SECRET, with_secret) run("none", None, without) - for i, bad in enumerate(("short", SECRET[:-1] + "g", SECRET + "0", SECRET.upper(), - SECRET[:16] + "\r\nX-Evil: 1\r\nX-Pad: 12", - SECRET[:16] + "\r\nX-Evil: 1\r\nX:1"), 1): # the last is 32 long + for i, bad in enumerate((SECRET[:-1] + "g", + SECRET[:16] + "\r\nX-Evil: 1\r\nX:1"), 1): # the second is 32 long run("malformed-%d" % i, bad, without) run("runner", SECRET, with_secret, runner=True, handed=SECRET) run("runner-none", None, without, runner=True) - run("runner-malformed", SECRET[:16] + "\r\nX-Evil: 1\r\nX:1", without, runner=True) print("cool_report_test: all passed") diff --git a/scripts/bench/ctlport_test.py b/scripts/bench/ctlport_test.py index fe853ed..463d3af 100644 --- a/scripts/bench/ctlport_test.py +++ b/scripts/bench/ctlport_test.py @@ -26,25 +26,23 @@ job after it. 5. the port has one client: a second connection is refused and the first keeps working (it never displaces, unlike the Grbl socket); a client that closes and reconnects at once is served - 6. a CR LF sender keeps its count across a port jog, an empty line - included - 7. the status hook survives a soft reset (the core puts its own report + 6. the status hook survives a soft reset (the core puts its own report handlers back at every reset) - 8. the port's client is the dead-man: closing it cancels its jog - 9. every port operation ships dark. Under an open armed window, with M3 + 7. the port's client is the dead-man: closing it cancels its jog + 8. every port operation ships dark. Under an open armed window, with M3 modal and S above zero, a port jog, a canceled port jog, the refused jogs, and the panel operations (the tray's mode among them) add steps to the shipped stream and not one FIRE tick. A jog block carries the modal spindle state, so the stream's mask on jogging is the only thing that keeps a jog dark, and this is what holds the port to it - 10. "sender out" keeps senders from the network out, and only on an idle + 9. "sender out" keeps senders from the network out, and only on an idle machine with a quiet sender: it is refused while a sender line runs (a dwell), within 2 s of the sender's last line, and while a program - moves; granted, it drops the sender after a message line, turns every - sender from the network away the same way without touching the - session, still lets one from this host in, outlives the port's client, - and "sender in" ends it. A sender from the network is one that - connects to this host's own address rather than the loopback + moves; granted, it drops the sender after a message line, outlives + the port's client, and "sender in" ends it. A sender from the network + is one that connects to this host's own address rather than the + loopback; one turned away while senders are kept out is serial_test.c's + (grblHAL-glowforge) Usage: ctlport_test.py [path/to/grblHAL_glowforge] """ @@ -469,10 +467,6 @@ def poll_case(tag, eol): s.close() -def test_poll_lf(): - poll_case("poll-lf", "\n") - - def test_poll_crlf(): poll_case("poll-crlf", "\r\n") @@ -541,30 +535,6 @@ def test_single_client(s): "five reconnects right after a close were each served") -def test_crlf(): - s = Session(eol="\r\n") - try: - for line in ("G91", "G0 X1", ""): - if s.sender.send(line) != "ok": - fail("[crlf] %r did not get ok" % line) - s.sender.wait_state("Idle") - s.quiet() - before = s.sender.count() - if s.port.request("jog G91 X1 F600") != "ok": - fail("[crlf] the port jog was refused") - s.sender.wait_state("Idle") - for line in ("", "G0 X1", "", ""): - if s.sender.send(line) != "ok": - fail("[crlf] %r after the port jog did not get ok" % line) - time.sleep(0.5) - got = s.sender.count() - before - if got != 4: - fail("[crlf] 4 sender lines after the port jog got %d responses" % got) - print("PASS [crlf]: a CR LF sender's count is exact across a port jog, empty lines included") - finally: - s.close() - - def test_port_dark(): s = Session(laser=True) try: @@ -664,11 +634,6 @@ def own_address(): return addr -def generation(s): - with open(os.path.join(s.workdir, "grbl.state")) as f: - return json.load(f)["sender"]["generation"] - - def wait_closed(sender, what, timeout=3.0): end = time.time() + timeout while time.time() < end and not sender.closed: @@ -711,7 +676,6 @@ def test_sender_out(): print("PASS [sender-out]: refused during a dwell, within 2 s of a line, and while a program moves") time.sleep(2.3) - gen = generation(s) got = s.port.request("sender out") if got != "ok": fail("[sender-out] an idle machine and a quiet sender: %r, wanted ok" % got) @@ -723,21 +687,6 @@ def test_sender_out(): fail("[sender-out] the state after it: %r" % st) print("PASS [sender-out]: granted, it dropped the sender after a message line") - gen = generation(s) - late = Sender(PORT, host=own) - wait_closed(late, "a sender from the network") - if not late.saw("[MSG:The machine is in use") or late.count(): - fail("[sender-out] the turned-away sender read %r" % late.other) - time.sleep(0.3) - if generation(s) != gen or s.port.state().get("sender") is not False: - fail("[sender-out] turning a sender away changed the session") - local = Sender(PORT) - if local.closed or local.send("G21") != "ok": - fail("[sender-out] a sender on this host was not let in") - local.close() - print("PASS [sender-out]: a sender from the network is turned away and the session is " - "untouched; one from this host connects") - s.port.close() s.port = PortClient(s.path) if s.port.state().get("sender_out") is not True: @@ -773,8 +722,6 @@ def main(): test_dead_man(s) finally: s.close() - test_crlf() - test_poll_lf() test_poll_crlf() test_port_dark() test_sender_out() diff --git a/scripts/bench/envelope_test.py b/scripts/bench/envelope_test.py index be84556..637f699 100644 --- a/scripts/bench/envelope_test.py +++ b/scripts/bench/envelope_test.py @@ -9,21 +9,20 @@ Runs the native grblHAL_glowforge binary in null-sink mode, with the scripted sender and the port client of ctlport_test.py, homing manually (nothing moves; X and Y become homed and the envelope is set): - 1. unset keys: X's and Y's far edges are the travel ($130, $131) - 2. envelope_x_mm and _y set the far edges at a home, and a jog past one - is the core's error:15 - 3. a key below GFHOME_ENVELOPE_MIN_MM is held to it, and one past the - travel plus GFHOME_ENVELOPE_EXTRA_MM is held to that - 4. the port's envelope open: refused before a home (error:homed); after + 1. envelope_x_mm and _y set the far edges at a home, and a jog past one + is the core's error:15 (an unset key being the travel, and a key held + to GFHOME_ENVELOPE_MIN_MM and to the travel plus + GFHOME_ENVELOPE_EXTRA_MM, are lens_home_test.c's in grblHAL-glowforge) + 2. the port's envelope open: refused before a home (error:homed); after one, the port's jogs reach the travel plus 30 mm for X and Y, and the port's state says the envelope is open; envelope apply puts the keys' edges back without a home - 5. an open envelope is the port's jogs' alone: a status poll and an + 3. an open envelope is the port's jogs' alone: a status poll and an empty line leave it open, and the sender's first line closes it before the core reads the line (the line then answered ok, and the keys' edges in force), a port jog it meets canceled - 6. a soft reset and the port client going away each close it - 7. envelope open is refused under an open armed window (busy:state) + 4. a soft reset and the port client going away each close it + 5. envelope open is refused under an open armed window (busy:state) Usage: envelope_test.py [path/to/grblHAL_glowforge] """ @@ -111,14 +110,6 @@ def saw_within(s, needle, seconds=2.0): return s.sender.saw(needle) -def test_unset(s): - conf(s) - home(s) - edge(s, "X", TRAVEL_X - 1, TRAVEL_X + 1, "unset") - edge(s, "Y", TRAVEL_Y - 1, TRAVEL_Y + 1, "unset") - print("PASS [unset]: with no key the far edges are the travel, %.0f and %.0f" % (TRAVEL_X, TRAVEL_Y)) - - def test_measured(s): conf(s, envelope_x_mm=480, envelope_y_mm=284.5) home(s) @@ -127,14 +118,6 @@ def test_measured(s): print("PASS [measured]: envelope_x_mm 480 and envelope_y_mm 284.5 (past the travel) are the far edges at a home") -def test_bounds(s): - conf(s, envelope_x_mm=10, envelope_y_mm=900) - home(s) - edge(s, "X", 49, 51, "bounds") - edge(s, "Y", TRAVEL_Y + EXTRA - 1, TRAVEL_Y + EXTRA + 1, "bounds") - print("PASS [bounds]: a key below 50 mm is held to 50, one past the travel plus 30 mm to that") - - def test_open_apply(s): conf(s, envelope_x_mm=480, envelope_y_mm=250) home(s) @@ -249,9 +232,7 @@ def main(): fail("no controller binary at %s" % ct.BIN) s = ct.Session() try: - test_unset(s) test_measured(s) - test_bounds(s) test_open_apply(s) test_sender_closes(s) test_reset_closes(s) diff --git a/scripts/bench/laser_lifecycle_test.py b/scripts/bench/laser_lifecycle_test.py index 21c4644..a91b995 100644 --- a/scripts/bench/laser_lifecycle_test.py +++ b/scripts/bench/laser_lifecycle_test.py @@ -22,7 +22,7 @@ reported messages: 5. arming refused while the cooling verdict blocks fire, or before the engine acknowledges the session: laser_arm_test.c cases C, A2 and A3, and cooling_test.c's arm-time gate; the wait for the fans - is this harness's + is laser_arm_test.c case A4 6. with a switch source present (GF_SWITCH_FILE), the arm blocks on the button: a press with the lid closed arms, and the lid or the interlock loop opening during the wait cancels the job (a clean @@ -558,32 +558,6 @@ def test_rx_overrun_aborts(): s.close() -def test_arm_waits_for_the_fans(): - """Rule 5: the acknowledgment budget follows the fan grace. The - engine acknowledges once the fans are at their floors, seconds after - the arm; with cool_fan_grace_s at 8 the controller waits up to 13 s - for it, and an acknowledgment at 9 s arms the job rather than - refusing it at the old 5 s budget.""" - ack_s = 9.0 - s = Session("arm-fans", fire_ok=True, ack_after_s=ack_s, - conf_extra="cool_fan_grace_s = 8\n") - try: - t0 = time.time() - s.send_raw("M4 S100") - s.send_raw("G1 X1 F600") - if not wait_for(s.log, ARMED, 20, s.sock): - fail("[arm-fans] the arm never completed with the acknowledgment at %.0f s " - "(the budget did not follow cool_fan_grace_s)" % ack_s) - dt = time.time() - t0 - if dt < ack_s - 0.5: - fail("[arm-fans] armed after %.1f s, before the fans were up at %.1f s" % (dt, ack_s)) - if BLOCKED in "".join(s.log): - fail("[arm-fans] the wait for the fans was reported as a refusal") - print("PASS [arm-fans]: the arm waited %.1f s for the fans, then armed" % dt) - finally: - s.close() - - def test_sigterm_mid_job(): """Rule 5: a termination signal during an armed job is a STOP, not a "finish the job first". The supervisor's expected-stop path (mode @@ -980,7 +954,6 @@ def main(): test_lid_cancels_and_returns() test_pause_then_lid_cancel_returns_to_the_job_start() test_lid_policy_hold() - test_arm_waits_for_the_fans() test_sigterm_mid_job() print("PASS: the armed-window lifecycle holds") diff --git a/scripts/bench/manual_home_test.py b/scripts/bench/manual_home_test.py index 8594eaa..f0a5fa2 100644 --- a/scripts/bench/manual_home_test.py +++ b/scripts/bench/manual_home_test.py @@ -29,8 +29,9 @@ operator's own $ME or a manual $H energizes it: the origin lies, never negative. With an offset the head at the stops is declared at minus it and jogs to the origin, dark, before $H answers, through the sender and through the port; the envelope starts - at the origin, and an out-of-range offset is clamped; a jog cancel - ends the move short of the origin and $H says where it stopped + at the origin (an out-of-range offset clamped is lens_home_test.c's, + gfhome_clamp_home_mm); a jog cancel ends the move short of the origin + and $H says where it stopped 9. a controller that starts over a released gantry (the marker in the state directory) comes up locked and writes no current until $ME 8. each provider reads its own offsets and no other's: with both pairs set, @@ -426,15 +427,6 @@ def test_stop_block_offsets(s): r = refused(s, line) if r != "error:15": fail("[offsets] %s, behind the origin, got %r, not error:15" % (line, r)) - s.set_mode("manual", "manual_home_x = -10.3\nmanual_home_y = 1e9\n") - if s.sender.send("$H", timeout=30.0) != "ok": - fail("[offsets] $H with out-of-range offsets was refused") - st = s.port.state() - if st["mpos"][0] != 0 or st["mpos"][1] != 0: - fail("[offsets] the head did not end at the origin: %r" % st["mpos"][:2]) - d = declared(s) - if d is None or d[0] != 0 or abs(d[1] + 279.0) > 0.01: - fail("[offsets] a negative offset and an oversized one declared %r, not (0, -279)" % (d,)) # The panel's Set home here: the port answers once the head has stopped, # and the sender's count stays exact. s.set_mode("manual", "manual_home_x = 12.5\nmanual_home_y = 8\n") @@ -449,8 +441,8 @@ def test_stop_block_offsets(s): fail("[offsets] the sender got %d responses for the port's home" % (s.sender.count() - before)) s.set_mode("manual") print("PASS [offsets]: a manual home declared (-12.5, -8) and jogged to the origin before $H " - "answered, dark under a modal M3; the envelope started at the origin; a negative offset " - "became 0 and an oversized one the axis travel; the port's home answered at the origin") + "answered, dark under a modal M3; the envelope started at the origin; the port's home " + "answered at the origin") def test_origin_jog_cancel(s): diff --git a/scripts/bench/mcode_test.py b/scripts/bench/mcode_test.py index 4348971..4532a7e 100644 --- a/scripts/bench/mcode_test.py +++ b/scripts/bench/mcode_test.py @@ -24,12 +24,16 @@ sender and for the machine daemon: [MSG:] of its words, and a cycle start resumes it 5. a soft reset ends the wait at once, and the controller answers the next line - 6. no answer in GFMCODE_WAIT_S holds the job + 6. no answer in GFMCODE_WAIT_S holds the job: 5 s in the host build + (configured with -DGFMCODE_WAIT_S=5.0, as grblHAL-glowforge's CI + does; the image keeps 30 s), longer than any case here waits at a + barrier on purpose 7. the barrier is dark: under an open armed window with M3 S500, a job waiting at an M-code ships no FIRE tick, the window stays open across the wait, and the next laser move fires again Usage: mcode_test.py [path/to/grblHAL_glowforge] + (a build configured with -DGFMCODE_WAIT_S=5.0) """ import os import sys @@ -39,7 +43,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import ctlport_test as ct # noqa: E402 fail = ct.fail -WAIT_S = 30.0 +WAIT_S = 5.0 # the host build's GFMCODE_WAIT_S def send_nowait(s, line): @@ -66,7 +70,9 @@ def test_table(s): r = s.port.request("mcodes " + good) if r != "ok": fail("[table] mcodes %s -> %r" % (good, r)) - for bad in ("159", "180", "160,160", "160,", ",160", "16a", "0160", "160 161", "1600", "160,,161", ""): + # one form per refusal: out of range, twice, a separator with nothing + # after it, not a number, and nothing at all + for bad in ("159", "160,160", "160,", ",160", ""): r = s.port.request("mcodes " + bad) if r != "error:invalid": fail("[table] mcodes %r -> %r, not error:invalid" % (bad, r)) @@ -81,8 +87,8 @@ def test_table(s): time.sleep(0.02) if s.sender.responses[n:n + 1] != ["ok"]: fail("[table] M165 got %r" % s.sender.responses[n:]) - print("PASS [table]: the port takes '-' and numbers from M160 to M179, the whole range; 11 other forms " - "refused, the table left as it was") + print("PASS [table]: the port takes '-' and numbers from M160 to M179, the whole range; a form " + "of each refusal is refused, the table left as it was") def test_unanswered(s): @@ -198,9 +204,9 @@ def test_timeout_holds(s): wait_pending(s) s.sender.wait_state("Hold", timeout=WAIT_S + 5) dt = time.time() - t0 - if dt < WAIT_S - 1: + if dt < WAIT_S - 0.1: fail("[timeout] the job was held after %.1f s, not after the wait" % dt) - if not s.sender.saw("M160 had no answer from its extension in 30 s: the job is held"): + if not s.sender.saw("M160 had no answer from its extension"): fail("[timeout] the sender got no [MSG:] of the timeout") s.sender.realtime(b"~") s.sender.wait_state("Idle", timeout=5) diff --git a/scripts/bench/planner_blocks_test.py b/scripts/bench/planner_blocks_test.py index f166938..dddb32e 100644 --- a/scripts/bench/planner_blocks_test.py +++ b/scripts/bench/planner_blocks_test.py @@ -18,12 +18,10 @@ Runs the native grblHAL_glowforge binary in null-sink mode (no hardware, no root) and drives it over TCP, keeping one settings store across restarts: - 1. a fresh store starts and reports its default $398 - 2. $398=400 is accepted and read back - 3. the controller restarted on that store answers on the port, reports - 400 blocks, and the status report shows a planner that deep - 4. the same at $398=1000, the top of the range - 5. a motion line runs to completion on the deep buffer + 1. $398=1000, the top of the range, is accepted and read back + 2. the controller restarted on that store answers on the port, reports + 1000 blocks, and the status report shows a planner that deep + 3. a motion line runs to completion on the deep buffer A controller that connects and never answers after the restart is the failure this harness exists for. Every exit kills the controller it @@ -50,7 +48,6 @@ PORT = 2401 CONNECT_S = 8.0 ANSWER_S = 5.0 -DEEP = 400 TOP = 1000 _live = None # the controller process a failure has to take down @@ -199,15 +196,6 @@ def main(): fail("no controller binary at %s" % BIN) workdir = tempfile.mkdtemp(prefix="planner-blocks-") try: - s = Session(workdir) - check(s.answers(), "a fresh store starts and answers", - "a fresh store does not answer") - print(" default $398=%s" % s.setting("$398")) - s.stop() - - s = restart_on(workdir, DEEP) - s.stop() - s = restart_on(workdir, TOP) reply = s.send("G91 G1 X1 F600", 1.5) check("ok" in reply and "error" not in reply and "ALARM" not in reply, @@ -216,7 +204,7 @@ def main(): check(s.wait_idle(), "the move completes and the controller is Idle", "the controller did not return to Idle after the move") s.stop() - print("PASS: the planner buffer starts at %d and %d blocks" % (DEEP, TOP)) + print("PASS: the planner buffer starts at %d blocks" % TOP) finally: if _live is not None: _live.stop() diff --git a/scripts/bench/tray_test.py b/scripts/bench/tray_test.py index 5a22b60..f4daa47 100644 --- a/scripts/bench/tray_test.py +++ b/scripts/bench/tray_test.py @@ -21,8 +21,8 @@ sender on the Grbl socket, and the port client of ctlport_test.py: the lens's reach, shifted, run and two half-steps past them are refused, and a jog to a tray-in Z is refused; with the tray in again the same jog runs - 3. a missing P, a P other than 0 or 1, and an axis word on the line are - errors (28, 39, 31) that change nothing + 3. a P other than 0 or 1, and an axis word on the line, are errors (39, + 31) that change nothing 4. the M-code is a barrier: behind a move it waits for the move to end, and the frame changes only once the machine is idle 5. the port's tray op switches the mode with a sender connected, the @@ -33,7 +33,7 @@ sender on the Grbl socket, and the port client of ctlport_test.py: 6. the mode survives a soft reset, and a controller restart: a start with the marker present takes the lens reference in the tray-out frame, and one without it in the tray-in frame - 7. tray_offset_mm sets the shift, held to 13 to 60 mm + 7. tray_offset_mm sets the shift 8. an unreferenced lens shifts the same way, and stays pinned where it reads 9. a camera home with the tray out declares Z the offset higher, and @@ -259,8 +259,7 @@ def test_envelope(s): def test_errors(s): ticks = s.ticks() - for line, want in (("M103", "error:28"), ("M103 P2", "error:39"), ("M103 P0.5", "error:39"), - ("M103 P-1", "error:39"), ("M103 P1 Z5", "error:31"), ("M103 P1 X5", "error:31")): + for line, want in (("M103 P2", "error:39"), ("M103 P1 X5", "error:31")): r = refused(s, line) if r != want: fail("[errors] %r got %r, not %s" % (line, r, want)) @@ -270,7 +269,7 @@ def test_errors(s): time.sleep(0.3) if s.ticks() != ticks: fail("[errors] the refused lines shipped %d step ticks" % (s.ticks() - ticks)) - print("PASS [errors]: a missing P, P2, P0.5, P-1 and an axis word were refused and changed nothing") + print("PASS [errors]: P2 and an axis word were refused and changed nothing") def test_barrier(s): @@ -391,7 +390,7 @@ def test_persistence(s): def test_offset(): - for key, k in (("20", 58), ("5", 38), ("99", 175)): + for key, k in (("20", 58),): s = Session(conf={"tray_offset_mm": key}) try: s.sender.send("M103 P1") @@ -400,8 +399,7 @@ def test_offset(): expect_z(s, EDGE_K, "offset %s: back in" % key) finally: s.close() - print("PASS [offset]: tray_offset_mm 20 moved Z 58 half-steps, 5 was held to 13 mm (38), " - "99 to 60 mm (175)") + print("PASS [offset]: tray_offset_mm 20 moved Z 58 half-steps") def test_unreferenced(): diff --git a/scripts/bench/xy_mode_test.py b/scripts/bench/xy_mode_test.py index 1d5429d..969e6a7 100644 --- a/scripts/bench/xy_mode_test.py +++ b/scripts/bench/xy_mode_test.py @@ -11,15 +11,15 @@ never takes them typed: $100/$101, the machine tick, and the kernel stop ramp. This harness drives the native grblHAL_glowforge binary in null-sink mode (no hardware, no root) over TCP, one process per config: - 1. no key: x32, $100/$101 = 213.333, the 112640 Hz tick - 2. xy_microsteps = 16: 106.667, the 56320 Hz tick; a typed $100 is - overwritten on the spot; $110 is left alone under a tick that - carries it - 3. xy_microsteps = 32: 213.333, the 112640 Hz tick - 4. a value that is not a mode: x32 with a warning in the log - 5. GFSINK_RATE lowered under the mode's tick: $110/$111 are held at + 1. no key: x32, $100/$101 = 213.333, and $110 stands + 2. xy_microsteps = 16: 106.667; a typed $100 is overwritten on the + spot; $110 is left alone under a tick that carries it + 3. GFSINK_RATE lowered under the mode's tick: $110/$111 are held at the feed the tick carries, one step per tick per axis +Each mode's derivations, and a value that is not a mode, are the host +unit test's (xy_scale_test.c in grblHAL-glowforge). + The binary keeps its settings in EEPROM.DAT in the working directory, so each run starts from defaults in a temporary directory and leaves nothing behind. @@ -142,10 +142,6 @@ def expect_scale(s, mode, spm): "%s is %.3f at x%d, want %.3f" % (key, got, mode, spm)) -def expect_log(log, needle, what): - check(needle in log, what, "the log does not say %r:\n%s" % (needle, log)) - - def case_default(): s = Session() try: @@ -154,8 +150,7 @@ def case_default(): check(near(rate, 12000.0), "$110=%.0f stands at x32" % rate, "$110 is %.3f at x32, want 12000" % rate) finally: - log = s.close() - expect_log(log, "x32 microsteps, 112640 Hz machine tick", "no key: x32, the 112640 Hz tick") + s.close() def case_x16(): @@ -172,27 +167,7 @@ def case_x16(): check(near(rate, 12000.0), "$110=%.0f stands under a tick that carries it" % rate, "$110 is %.3f under x16, want 12000" % rate) finally: - log = s.close() - expect_log(log, "x16 microsteps, 56320 Hz machine tick", "x16: the 56320 Hz tick") - - -def case_x32(): - s = Session("xy_microsteps = 32\n") - try: - expect_scale(s, 32, 213.333) - finally: - log = s.close() - expect_log(log, "x32 microsteps, 112640 Hz machine tick", "x32: the 112640 Hz tick") - - -def case_invalid(): - s = Session("xy_microsteps = 24\n") - try: - expect_scale(s, 32, 213.333) - finally: - log = s.close() - expect_log(log, "xy_microsteps '24' is not 8, 16 or 32", "a value that is not a mode is refused with a warning") - expect_log(log, "x32 microsteps, 112640 Hz machine tick", "and the machine runs at x32") + s.close() def case_low_tick(): @@ -211,9 +186,7 @@ def case_low_tick(): check(near(got, 562.5, 0.05), "a typed $110=6000 comes back held at %.3f" % got, "a typed $110=6000 stuck at %.3f under a 1000 Hz tick" % got) finally: - log = s.close() - expect_log(log, "1000 Hz machine tick", "GFSINK_RATE took the tick to 1000 Hz") - expect_log(log, "held at the ceiling", "the hold is logged") + s.close() def main(): @@ -221,8 +194,6 @@ def main(): fail("no controller binary at %s" % BIN) case_default() case_x16() - case_x32() - case_invalid() case_low_tick() print("PASS xy_mode_test")