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.
This commit is contained in:
ScottW514
2026-10-13 21:20:31 -04:00
parent 520d62ac05
commit 8e1fe5090d
9 changed files with 66 additions and 216 deletions
+8 -14
View File
@@ -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 <path-to-grblHAL_glowforge>
"""
@@ -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")
+9 -62
View File
@@ -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()
+8 -27
View File
@@ -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)
+1 -28
View File
@@ -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")
+5 -13
View File
@@ -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):
+13 -7
View File
@@ -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)
+5 -17
View File
@@ -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()
+7 -9
View File
@@ -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():
+10 -39
View File
@@ -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")