mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-10-19 02:51:42 -07:00
9c97f05db9dacd79329c3570bc0397c331c99321
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9c97f05db9 |
forgetest: the lint counts a step as run only when every job its job needs runs
The coverage lint judges a HOST_TESTED row by a step of its workflow
that runs on every run of the workflow. It read the `if:` and the
`continue-on-error:` of a step and of its job, and not the job's
`needs:`. GitHub skips a job whose needed job did not run (an `if:`
that held it back, or a failure), unless the job says `always()`, so
a test step in a job that needs a conditional job runs on not every
push, and counted all the same: a host test behind a conditional
build keeps its row green and its paths covered. No workflow a row
names uses `needs:`.
workflow_steps now reads each job's `needs:` (_needs: a flow list,
one scalar, or a block list), keeps the jobs by name, and resolves
them when the workflow is read (_needs_run): a job's needs are met
on every run when every job it names runs on every run, that is,
carries no `if:` other than the literal true and has its own needs
met, transitively. A job the workflow does not have, a cycle, and a
`needs:` in a form the lint does not read each fail closed. A needed
job's `continue-on-error:` does not bear: its failure fails neither
the run nor the jobs after it. The job's dict carries the result as
"needs_run", and _on_every_run counts no step of a job whose needs
are not met, so workflow_runs and checkout_path refuse it as they
refuse a conditional step. The reason stays "does not run it". The
lint evaluates no condition, so a dependent job under `always()`
counts as no job, as a step under `always()` counts as no step. The
HOST_TESTED comment and the docstrings say what the lint reads.
Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s
tests -v` in forgetest): test_manifest.py runs 40 tests, OK, on
Ubuntu 24.04 with python 3.12, in a private PID namespace. The new
case test_a_job_runs_only_when_every_job_it_needs_runs: a job that
needs an unconditional job runs its test, with the `needs:` as a
scalar, a flow list, a quoted flow list and a block list; the same
four with the needed job under `if: false` or `if: github.event_name
== 'push'` do not; a needed job under `continue-on-error: true`
runs it; a chain of three runs it, and does not with the first job
under `if: false`; a needed job the workflow does not have, a cycle
of two, a self-need and a `needs:` holding a mapping each do not;
`if: always()` on the dependent job, and on the middle job of the
chain, does not. test_the_workflow_has_to_run_the_host_test gains a
workflow whose test job needs a job under `if: false`, listed as
"does not run it". Control: the parent,
|
||
|
|
e1d1c1b84e |
forgetest: motion.jog-roundtrip's ramp comment follows the kernel's stepped ramp
The soft-reset check's comment described the kernel's ramp with the
continuous formula: (f0^2 - 900^2) / (2 a) ticks, 12687 over 223 ms
at the x32 tick, and a part-way run-out at f0 * sqrt(1 - 2 (a / f0)
Q). The kernel's ramp is stepped (cnc.c, ramp_update_tasklet_fn): the
tick holds for the first 10 ms after the stop, then every 10 ms drops
by ramp_rate / 100 until it reaches the 900 Hz floor, which plays
10 ms before the stop. At 112640 Hz and 500000 Hz/s that is 5000 Hz
off every 10 ms: 24 intervals, 240 ms, about 13270 ticks, 118 ms of
the tick. The comment now says so, with the figures that follow: the
rate at a run-out is 24% of the tick with 110 ms of queue left, 38%
with 100 ms, 51% with 90 ms; the tail's mean over the 25 to 30 ms the
poll samples is 32%, 45% and 56% (61% over the 50 ms window bound),
not 21% to 56%; the 70% bound is 14 points above the slowest ramped
tail and 30 below a run-out; the time to leave running is the 100 ms
hold plus the run's end on the ramp, about 240 ms with 100 ms left,
270 ms with 110 ms, 340 ms at the floor. The evidence's ramp_ticks and
ramp_s stay the continuous figures, and the comment says they are,
about 4% and 7% under the stepped ramp's. No judge changes: the 70%
bound, the 25 ms window and the 50 ms gap stay.
The Alarm wait's failure text names a sampling stall: when the 5 ms
sampling loop runs past 2 s after the ^X, the wait has no time to
read a state, and the failure read "not in Alarm within 2 s (state
None)". It now says the state is unread because the sampling loop ran
past the 2 s. The condition is the same; the text is the change.
Proof: a compile and pyflakes 4.0.3 give no message on
forgetest/suite/motion.py, with no undefined name in forgetest or
tests. forgetest unit tests, python3 -B -m unittest discover -s tests
-p test_<name>.py, in a private PID namespace: test_manifest.py 39
OK, test_queue.py 15 OK, test_campaign.py 13 OK. Coverage lint on the
tree manifest from scripts/manifest-from-tree.py (python3 -B -m
forgetest.coverage --manifest ../tree-manifest.json --enforce):
0 uncovered, 0 empty, 0 host-tested row problems, 43 tests. Host dry
runs of the whole test against the null-sink build of the
grblHAL-glowforge default branch head ("serial_test: the scan's exempt
arguments each match once in the sources"), with the machine side
stood in for as before: with a model kernel that ramps from the
controller's cnc/stop write, the test passes, MPos X 3502.07 steps
against 3502, tail 22% of the tick; with a model kernel that ramps as
cnc.c does, 5000 Hz off every 10 ms from 110 ms of queue, the test
passes, MPos X 3496.95 steps against 3497, tail 33% of the tick, the
figure the comment gives for a run-out from 110 ms of queue.
Acceptance: the comment and the message text are inside the test's
own function, so the shared text of suite/motion.py does not change.
The fingerprint of motion.jog-roundtrip moves, and no other test's:
of the implementation hashes of the 43 catalog tests, before and
after, only that one differs.
|
||
|
|
b4493f5052 |
forgetest: motion.jog-roundtrip's covers name the forgectrl route its homed-mask judge reads
The soft-reset check's reference verdict is the homed mask, read before
and after the reset through forgectrl's GET /motion/state
(cb_motion_state in src/main.c, through motion_port to the controller
port's state record), because $# prints the mask only with $22 on,
which the driver leaves off. A test that depends on a forgectrl route
names ("forgectrl", "src/main.c") in its covers, as microstep_modes
and deadman do, so that a change to the route invalidates the test's
inherited PASS. motion.jog-roundtrip's covers are now _MOTION_COVERS
plus that entry.
Proof: a compile and pyflakes 4.0.3 give no message on
forgetest/suite/motion.py, with no undefined name in forgetest or
tests. forgetest unit tests, python3 -B -m unittest discover -s tests
-p test_<name>.py, in a private PID namespace: test_manifest.py 39
OK, test_queue.py 15 OK, test_campaign.py 13 OK. Coverage lint on the
tree manifest from scripts/manifest-from-tree.py (python3 -B -m
forgetest.coverage --manifest ../tree-manifest.json --enforce):
0 uncovered, 0 empty, 0 host-tested row problems, 43 tests; the
catalog loads motion.jog-roundtrip with the five covers entries. A
host dry run of the whole test against the null-sink build of the
grblHAL-glowforge default branch head ("serial_test: the scan's exempt
arguments each match once in the sources"), with the machine side
stood in for as before and a model kernel that ramps from the
controller's cnc/stop write, passes: MPos X 3534.93 steps against
3535, tail 23% of the tick, mask 4 kept, drift 0.000 mm.
Acceptance: the covers are part of the test's own text, so the
fingerprint of motion.jog-roundtrip moves, and no other test's: of
the implementation hashes of the 43 catalog tests, before and after,
only that one differs. From here a change under forgectrl's
src/main.c invalidates motion.jog-roundtrip's inherited PASS as well.
|
||
|
|
a3211ba1ea |
forgetest: motion.jog-roundtrip's read-back judge allows half a step, so a one-step miss always fails
The read-back judge of the soft-reset check compared MPos X in steps
with the kernel's X count to one step, and the Y and Z judges allowed
one step of MPos motion. One step is the smallest miss the read-back
can make: the core's kill step for the pulse start that never came,
which a position kept as the core counted it carries. MPos is
reported to 0.001 mm, so the difference of two readings carries up to
0.21 step at x32 (0.001 mm at $100 = 213.333; 0.05 step at x8), and
a one-step miss lands between 0.79 and 1.21 steps: on either side of
a one-step bound, at random. The bound is now half a step for X, Y
and Z: a correct read-back lands within 0.21 step, a one-step miss at
0.79 or more. The comment and the description say so.
With the committed judges the parent of the read-back work fails the
check on four counts, not two: the kernel is still running 1.0 s after
the ^X (the parent writes no cnc/stop), the tail rate is 100% of the
tick (the run ends at speed), the position is not read back (MPos X
one step past the kernel), and the homed mask goes 4 -> 0.
Proof: a compile and pyflakes 4.0.3 give no message on
forgetest/suite/motion.py, with no undefined name in forgetest or
tests. forgetest unit tests, python3 -B -m unittest discover -s tests
-p test_<name>.py, in a private PID namespace: test_manifest.py 39
OK, test_queue.py 15 OK, test_campaign.py 13 OK. Coverage lint on the
tree manifest from scripts/manifest-from-tree.py (python3 -B -m
forgetest.coverage --manifest ../tree-manifest.json --enforce):
0 uncovered, 0 empty, 0 host-tested row problems, 43 tests. Host dry
runs of the whole test against the null-sink build of the
grblHAL-glowforge default branch head ("serial_test: the scan's exempt
arguments each match once in the sources"), configured as its CI
configures it (-DGFMCODE_WAIT_S=5.0), with the head accelerometer,
the kernel's state and bytes-played counter, and forgectrl stood in
for and the step counters taken from the shipped stream: with a model
kernel that ramps from the controller's cnc/stop write, the test
passes, MPos X 3489.06 steps against 3489, tail 23% of the tick, mask
4 kept, drift 0.000 mm; with a model kernel that ramps as cnc.c does
(the tick held 10 ms, then 5000 Hz off every 10 ms to the 900 Hz
floor), the test passes, MPos X 3509.97 steps against 3510, tail 32%.
Against the null-sink build of the parent of "Stream engine: a soft
reset during motion gets the kernel's ramp, and the position is read
back from the kernel's counters" (the commit "Test hooks: honored only
in a null-sink run, through one gate"), under the ramp model, the
read-back judge fails 5 runs in 5, MPos X one step past the kernel
each time (1.06, 1.06, 1.09, 0.99 and 1.10 steps); with the one-step
judge the same parent passed the read-back 3 runs in 5 (misses of
1.09, 0.95, 0.90, 0.98 and 1.09 steps). Every parent run also fails
on the kernel still running 1.0 s after the ^X, the tail at 100%, and
the mask 4 -> 0. The real counters and the machine's timing are read
on the machine only.
Acceptance: the change is inside the test's own function and its
decorator, so the shared text of suite/motion.py does not change. The
fingerprint of motion.jog-roundtrip moves, and no other test's: of
the implementation hashes of the 43 catalog tests, before and after,
only that one differs. The check runs on the bench in the next
campaign as part of motion.jog-roundtrip.
|
||
|
|
84a231163a |
laser_lifecycle_test: a ^X during the web-service homing keeps the lens hold
Rule 18 (home-reset) sends a ^X during the web-service homing session
and reads what the reset leaves. The session runs with homing_mode =
gfcloud and a session command that sleeps (sleep 47.25, a command no
other process on the host runs), so the device is the session's for
as long as the rule wants it; Session drops GFSINK from the inherited
environment, which is what lets gfcloud_home_cmd run.
A reset during the session marks the position lost: the device is
the session's, and nothing is read back. Left set, the core's re-init
clears the homed mask, and the driver's Z-only hold with it, so a jog
after $X would move an unreferenced lens. The rule reads:
- before the session, a 1 mm Z jog is refused with error 15: the lens
has no reference in this session, so it is held where it stands;
- $H reports Home and the session's command is running;
- the ^X 0.5 s later lands in Alarm with ALARM:6, the session's
command is gone within 3 s, and the attribute log holds no new
"cnc/stop 1": the device was the session's, so the stream owed the
kernel no stop;
- $X recovers to Idle; the Z jog is refused with error 15 again and
MPos Z has not moved;
- a program move G91 G1 Z1 F100 raises ALARM:2, the soft limit of the
pinned lens; a ^X and $X then leave the controller Idle.
The verdicts are the error code, the alarm codes, MPos Z, the
attribute count and the process count; no log wording is judged.
Session gets mpos_z(), the Z of a status report, beside mpos_x().
The soft-limit alarm asks for a reset before $X unlocks it, and the
rule sends that ^X only once a status report has come from the
alarm's wait: a ^X on the heels of the ALARM line lands while the
soft limit's own reset is still pending and is folded into it.
Measured on the null sink, a ^X 0 ms or 20 ms after the ALARM line is
lost (the alarm's wait goes on, $X answers error 79) and one 100 ms
after it is taken. The '?' polls that wait for the reset banner keep
the controller's output flowing: in Alarm its protocol thread waits
up to 10 s for traffic before it drains its output.
Proof: python3 -m py_compile and pyflakes 4.0.3 are clean on
scripts/bench/laser_lifecycle_test.py. Against the null-sink build of
the grblHAL-glowforge default branch head, which carries "Stream
engine: a reset whose position stays lost pins the lens before the
core's re-init", configured as its CI configures it
(-DGFMCODE_WAIT_S=5.0), rule 18 passes 10 runs in 10 and the harness
passes whole, with no session command left running. Against the
build of that change's parent ("Stream engine: the step a reset owes
is taken only when the idle lands between interrupt callbacks") rule
18 fails 3 runs in 3, each at the Z jog after $X: the parent answers
ok and the lens moves to Z 1.027. The harness runs in
grblHAL-glowforge CI, step "Armed-window lifecycle" (python3
forgefirm/scripts/bench/laser_lifecycle_test.py
build/grblHAL_glowforge), from the head of this repository's default
branch: this commit is pushed before that driver change.
Acceptance: none. scripts/bench/ is outside every covers map.
|
||
|
|
c978e76b4c |
laser_lifecycle_test: rule 17's abort lands in a move above the step-smoothing cutoff
Rule 17's ^X mid-move goes out during G1 X120 F3000 instead of G1 X30 F600, and the plausibility bound on the distance the abort kept widens to the new move's length. Every other line of the rule stays. At $100 = 213.333 the F600 move cruises at about 2133 steps/s. The core's step smoothing puts a rate between 2000 and 4000 steps/s at level 2: four interrupt callbacks per step period, and only one in four carries an X step. The core counts a step one interrupt callback ahead of the pulse start that outputs it, so a reset leaves a pending step only when its idle lands on a callback that carries one, one run in four at F600. On the null sink the shipped backlog is empty by the time the reset runs and the residue is zero, so that pending step is all the position check can see, and the check is the harness's only proof of the driver's hand-over of the step at the idle and of the read-back correction. At F3000 the move cruises at 10667 steps/s, above the 8000 steps/s cutoff of level 0, where every callback carries a step and every reset owes one. The ^X goes out 0.5 s after the move reports Run and lands near 56 mm of the 120, inside the move. Proof: python3 -m py_compile and pyflakes 4.0.3 are clean on scripts/bench/laser_lifecycle_test.py. Against the null-sink build of the grblHAL-glowforge default branch head, configured as its CI configures it (-DGFMCODE_WAIT_S=5.0), rule 17 passes 10 runs in 10 and the harness passes whole. Two scratch mutants of that tree's driver.c, each built the same way: (a) readback_position keeps sys.position as the core counted it, with the position_lost handling kept; (b) stepperGoIdle hands the engine no pending step, the step and direction bits passed to gf_stream_go_idle forced to 0. With this change each fails rule 17 in 5 runs of 5, every time on the position check with MPos one step past the stream (17104.0 steps against 17103 for (a), 17197.0 against 17196 for (b), in their first runs). With the parent's F600 the same mutants pass rule 17 15 runs in 20 (a) and 14 runs in 20 (b). The harness runs in grblHAL-glowforge CI, step "Armed-window lifecycle" (python3 forgefirm/scripts/bench/laser_lifecycle_test.py build/grblHAL_glowforge), from the head of this repository's default branch. Acceptance: none. scripts/bench/ is outside every covers map. |
||
|
|
18cba1daa2 |
forgetest: motion.jog-roundtrip sends a soft reset during a move and judges the kernel's stop and the position read back
motion.jog-roundtrip gets a check after its hold/resume segment, with the head at the start and the machine at rest: a 60 mm +X G1 at F3000, and a ^X 0.3 s after the kernel starts to play it. At a soft reset during motion the grblHAL driver stops the kernel with its ramp, reads the position back from the kernel's step counters, and keeps the core's reference. No other catalog test sends a ^X while the head moves, and the null-sink harness cannot read real counters: its residue is zero by construction. The check fails when: - the controller is not in Alarm within 2 s of the ^X, or is Idle with no alarm (the move had ended before the ^X); - cnc/state does not leave running within 1.0 s, or leaves it for a state other than idle, or cnc/underruns or cnc/faults change across the check; - the kernel did not decelerate: over the run's last 25 to 50 ms, the bytes played (the position record, polled at 5 ms) are more than 70% of cnc/step_freq; - the ring holds bytes after the reset; - after $X, MPos X in steps ($100) and the kernel's X count differ by more than one step, or Y or Z moved; - the homed mask changes, or the sender gets a [MSG:] line that the position cannot be read back; - the jog back by the MPos distance leaves the kernel's X counter more than one step from its start. The head is jogged back before these judgments, so a failure of one of them leaves the head at the start. The ramp witness is the tail rate because of the arithmetic. At the stop the ring holds about 110 ms of ticks: the 200 ms queue and the 10 ms lead, less the 100 ms that the core's alarm hold plays. The ramp to the kernel's 900 Hz floor uses 112.6 ms of ticks, in every mode, because the ramp scales with the tick. So the run ends at the floor or runs out of data part of the way down the ramp, and the steps that never played can be zero. A minimum of unplayed steps in the driver's read-back log line, the bytes played after the ^X, and the time to leave running cannot tell the ramp from a run-out with a 20% margin. The log line is also an INFO line, which the log level setting can remove. The tail rate is 21% to 56% of the tick for 90 to 110 ms of queue at the stop, and 100% for a run-out. The other readings and the log line go to the evidence. The homed mask comes from the controller port's state record (forgectrl /motion/state): $# prints [HOME:...:mask] only with $22 on, and the driver's default is off. Before the reset the mask must hold the Z bit (the lens hold), because a mask without it cannot show a dropped reference. The test also requires kernel.k1-k2, the kernel's controlled stop on the SoC. The title and the description say what the test proves now. est_min stays 3. Proof: compile and pyflakes 4.0.3 give no message on suite/motion.py and no undefined name in forgetest or tests. forgetest unit tests, python3 -B -m unittest discover -s tests -p test_<name>.py, in a private PID namespace: test_manifest.py 39 OK, test_queue.py 15 OK, test_campaign.py 13 OK. Coverage lint on the tree manifest from scripts/manifest-from-tree.py (python3 -B -m forgetest.coverage --manifest ../tree-manifest.json --enforce): 0 uncovered, 0 empty, 0 host-tested row problems, 43 tests. A host dry run of the whole test against the null-sink build of grblHAL-glowforge f3158d3: the head accelerometer, the kernel's state and bytes-played counter, and forgectrl are stand-ins; the step counters are the steps of the shipped stream; /motion/state is the controller's own control port. With a model kernel that ramps from the controller's cnc/stop write, the test passes: tail 23% of the tick, MPos X 3496.1 steps against 3496, mask 4 kept, the read-back log line found, the X counter back at its start, drift 0.000 mm. With a model kernel that plays its queue to the end at the tick, the ramp check fails at 100%. Against the null-sink build of the parent (eb8eb61), the check fails on the mask (4 -> 0) and on the read-back (MPos X 3483.1 steps against 3482). The real counters, the ramp, and the machine's timing are read on the machine only. Acceptance: the change is inside the test's own function, so the shared text of suite/motion.py does not change. The fingerprint of motion.jog-roundtrip moves, and no other test's: of the implementation hashes of the 43 catalog tests, before and after, only that one differs. The new check runs on the bench in the next campaign as part of motion.jog-roundtrip. |
||
|
|
9c768619f8 |
laser_lifecycle_test: a ^X during a move asks for the kernel's stop, and keeps the position and the lens hold
Rule 17 (cancel-abort) reads three more witnesses of a soft reset
during motion. Its session runs with GFSINK_ATTR_LOG, the sysfs writes
that the hardware would get, beside GFSINK_DUMP.
- After the ^X, the attribute log holds one more "cnc/stop 1" than
before it: the controller asked the kernel to stop with its ramp.
- MPos X, in steps at $100, is the net X of the shipped stream within
half a step: the position kept is where the stream left the head, so
a step that the controller counted and never shipped fails the rule.
- After $X, a 1 mm Z jog is refused with error 15: the lens has no
reference in this session, so it is held where it stands, and the
abort keeps that hold.
The three new checks read no log wording: the attribute write, the
stream's bytes, MPos, and the jog's error code. The header's rule 17 says the same.
Session gets an attr_log option (GFSINK_ATTR_LOG, taken out of the
inherited environment otherwise) and three readers: setting(n) from a
$$ dump, attr_count(write) from the attribute log, and
shipped_x_steps() from the dump.
Proof: python3 -m py_compile and pyflakes 3.4.0 are clean on
scripts/bench/laser_lifecycle_test.py. The harness passes whole, on its
first attempt, against the null-sink build of grblHAL-glowforge with
the change that stops the kernel at a soft reset during motion and
reads the position back ("Stream engine: a soft reset during motion
gets the kernel's ramp, and the position is read back from the
kernel's counters", on top of eb8eb61), configured as its CI configures
it (-DGFMCODE_WAIT_S=5.0), and rule 17's session passes 10 runs in 10
there. Against the build of eb8eb61 the harness fails at rule 17, on
each of three attempts: "^X mid-move did not ask the kernel to stop
with its ramp (no cnc/stop write)". That build also accepts the Z jog
after the abort: a probe of it answered ok and then reported Z 1.027.
The position check, on scratch copies of the new driver: with the
core's pending step left out of the read-back, rule 17 fails 2 runs in
10, and with the read-back unapplied 3 runs in 10, each time with MPos
one step past the stream. The backlog is empty by the time the reset
runs, so a pending step is all that is left to miss, and a reset
leaves one only now and then. The harness runs in grblHAL-glowforge CI
(python3 forgefirm/scripts/bench/laser_lifecycle_test.py
build/grblHAL_glowforge) from the head of this repository's default
branch: push this commit first, then the driver change, as that
repository's AGENTS.md asks.
Acceptance: none. scripts/bench/ is outside every covers map.
|
||
|
|
79796b305b |
forgetest: the workflow_steps docstring says which list items the stray-quote rule reads
The docstring said a key with one stray quote as the first key of any list item makes every step count as no step. The parser reads a step's first key on its dash line and a list item outside a step, such as a matrix entry, and does not read a list inside a step's body, so a stray quote there leaves the step counted. The docstring now says so, as the Testing page does. Proof: forgetest-ci "Unit tests" step, test_manifest.py: 39 tests OK on the committed tree (python -B -m unittest discover -s tests -p test_manifest.py). pyflakes: clean. Coverage lint on the tree manifest: 0 uncovered, 0 empty, 0 host-tested row problems, 43 tests. The change is a docstring; no case changes. Acceptance: none; forgetest is dev-only and manifest.py is in no catalog fingerprint. |
||
|
|
7a71dd80b5 |
forgetest: the implementation hash walks a sibling imported as from . import x too
_imported walked a relative import only when it names a module (`from .x import y`): a level-1 ImportFrom with no module, `from . import x`, was passed over, so the suite module x joined no hash through it. The guard on the suite's package imports reads levels 2 and 0 alone, so it passed that spelling, and its own message sends an absolute import there: "import it relatively (from .. or from .)". So the guard and the walk did not read the same spellings: on a scratch copy of the catalog, forgectrl.auth's import of setup.py written `from . import setup` passed the guard, and an edit to setup.py's decode(), which the test calls, left forgectrl.auth's implementation hash where it was. homeoff.py imports cloud.py so (`from . import cloud as logs`); its `from .cloud import log_lines_since, log_size` on the next line keeps cloud.py in the walk, so no hash misses a module. _imported now walks every level-1 relative import: one with no module walks each name it imports as <suite>/<name>.py, the name and not its alias (a name that is no module of the suite is passed over, as a module was). The docstrings of _imported and sibling_imports, and the comment of the guard's case, name both spellings. Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s tests -v` in forgetest): test_manifest.py runs 39 tests, OK, on Windows with python 3.14 and on Ubuntu 24.04 with python 3.12. test_a_sibling_imported_inside_a_function_moves_the_tests_that_run_it gains a leg in which m.two imports judge.py as `from . import judge as j`: an edit to judge.py moves m.two and not m.one. Negative controls on scratch copies: catalog.py as it was fails that leg (the two hashes are equal), and so does the walk that reads the alias (j) in place of the name. On a scratch copy of the catalog with forgectrl.auth's import written `from . import setup`, an edit to setup.py's decode() now moves forgectrl.auth's hash. The coverage lint on the tree manifest from the recipe pins reports 0 uncovered paths, 0 host-tested row problems, 43 tests. pyflakes reports no undefined name. Acceptance: none. The source_sha of 43 of 43 tests is unchanged: the one module-less sibling import outside the registry (suite/__init__.py, which has no tests) is homeoff.py's, and cloud.py is in its walk already. catalog.py is the registry and no shared judge, forgetest is in no fingerprint, and tests/ are in no fingerprint. |
||
|
|
614efb23e4 |
forgetest: the lint counts a host test only on a line of run text whose failure ends the step
workflow_runs counted any simple command in a step's run text as
running the test: _SPLIT_RX split the text on &&, ||, ;, |, newlines,
then, do and else, and every part was matched. So a step that hid the
test's failure, or never ran it, kept a HOST_TESTED row green, against
the HOST_TESTED comment's "a step ... whose failure fails it":
`./build/tray_test || true`, `| tee log`, `&`, `true ||
./build/tray_test`, an `exit 0` or a `set +e` line before the test,
`if false; then ./build/tray_test; fi`, a here-document's lines, a
folded block (`run: >`) whose next line is `|| true`, and a step
under `shell: bash {0}` or a `defaults: run: shell:` of pwsh each
counted. The workflow_runs docstring said "Only what a step's run
commands execute counts", which a command the shell skips, or whose
status it drops, does not meet. No workflow of the tree uses these
forms at a row's test.
workflow_runs now reads the run text line by line as bash -e or sh -e
runs it (_run_commands), a backslash at the end of a line joining the
next. _shell_words reads a line's words and operators, with quotes and
expansions ($(...), ${...}, `...`) inside the words. A command counts
on a line that holds it alone, with its redirections, or in a pipeline
under pipefail (shell: bash, or `set -o pipefail` on an earlier line),
and only while errexit holds: a `set` of errexit or pipefail is
followed. A pipeline with no pipefail does not count. The lint stops at
a line with any other operator (`||`, `&&`, `;`, `&`, a subshell, a
here-document), at a command that opens a compound command or negates a
status (if, case, while, until, a group, a function, `!`), at a command
after which it cannot tell what a later line runs (exit, exec, trap,
eval, source, alias, shopt, hash and the like), and at a `set` of an
option it does not follow (noexec): nothing on that line or after it
counts. A for loop counts in one form: `for <name> in <words>; do` on
its own line, body lines that count (a pipeline under pipefail only),
and `done` on its own line. A step counts only when its own shell:,
and the `defaults: run: shell:` of its job and of the workflow, are
each bash, sh or absent: workflow_steps now reads those defaults, and
reads a folded block, or a scalar continued on the lines under its key,
as one line, as YAML folds it. A prefix is taken off only with the
flags with which it runs its command (`sudo -E`, `env -i`), so `command
-v ./build/tray_test`, which prints the path, does not count; exec is a
stop, no longer a prefix. The HOST_TESTED comment and the docstrings of
workflow_steps and workflow_runs say what the lint reads.
The lint does not read, and counts as no step: `./build/x; true`
(bash -e stops at the test, and the lint refuses the `;` all the
same), `exec ./build/x`, a loop on one line or with `do` on a line of
its own, a pipeline with the test last and no pipefail, a test under
`set +e` whose status a later line checks, a test after a guard line
with `||` or `&&`, and a `set` of any other option (`set -E`). It
counts without reading: what a loop's body does with the loop's name
(its list counts), a `cd` before the test, a `defaults: run:
working-directory:`, what PATH or BASH_ENV change, and the runner's
operating system (it does not read runs-on; a Windows runner's default
shell is pwsh).
Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s
tests -v` in forgetest): test_manifest.py runs 39 tests, OK, on Windows
with python 3.14 and on Ubuntu 24.04 with python 3.12.
test_a_step_counts_only_when_every_run_of_the_workflow_runs_it_and_its_failure_fails_the_run
gains these legs, none of which runs the test: `|| true`, `true ||`,
`; true`, `&` and a pipe with no pipefail on the test's line; an `exit
0`, `set +e`, `set -n` or here-document line before it; a `{` group
that `} || true` ends; a loop whose body is `python $t || true`, or a
pipe with no pipefail; a folded block and a continued scalar with `||
true` under the test; a step under `shell: bash {0}`; a job's and a
workflow's `defaults: run: shell: pwsh`. A pipe under `set -o
pipefail` and under shell: bash, and a step under shell: sh, run it.
test_what_running_a_host_test_looks_like gains `command -v
./build/tray_test`, which does not run it; its loop case is the loop as
python3-gfhardware's CI writes it (an assignment from `$(basename "$t"
.py)` in the body), and its glob case is a loop on lines of its own.
Negative controls on scratch copies: manifest.py as it was fails the
first new leg (True is not false), and each new leg reads as running
the test there; 19 mutations of the new reading each fail a case: exit
no stop, errexit not followed, an unknown `set` option followed, a
here-document read as a redirection, no reserved words, pipefail always
on, shell: bash with no pipefail, `set -o pipefail` not followed, the
loop's body not read, a body pipeline with no pipefail read, a folded
block read line by line, a continued scalar's lines dropped, the job's
defaults not read, the workflow's defaults not read, any shell read,
sh not read, `$(` read as an operator, every flag of a prefix taken
off, and `sudo -E` kept. Under bash -e with a failing stand-in test,
the step exits 0 for `|| true`, `true ||`, `&`, a pipe with no
pipefail, an earlier `exit 0`, `set +e` or `set -n`, the `{ } || true`
group, both loop bodies and the folded block; `; true` exits 1. A
here-document's lines are its command's input (`cat <<'EOF'` exits 0),
and `bash {0}` runs with no errexit, so a line after the test decides
the step (exit 0). The 17 workflows of the tree's repositories (211
steps) read to the same events and steps; of their 161 run steps, 20
count fewer commands and 4 the same commands with a redirection taken
off or an expansion kept as written, none of them a step that runs a
row's test. Each
of the 46 HOST_TESTED rows runs its test under the new reading, in the
workflows at the recipe pins and at the heads of forgectrl and
forgeext. The coverage lint on the tree manifest from the recipe pins
reports 0 uncovered paths, 0 host-tested row problems, 43 tests.
pyflakes reports no undefined name.
Acceptance: none. forgetest is dev-only and in no fingerprint (the
source_sha of 43 of 43 tests is unchanged); the lint runs in CI, not in
a campaign, and tests/ are in no fingerprint.
|
||
|
|
3aecf800a5 |
forgetest: the lint's stray-quote rule reads a step's first key, on its dash line, too
The rule that a key with one stray quote makes every step of the
workflow count as no step never saw a step's first key, the one on its
dash line. _STEP_RX reads no key with a stray quote (`- run": make`,
`- name": Build`), and _stray_key cannot match a line that starts with
`- `. So such a step dropped out with no "unread" set, and the
workflow's other steps still counted. YAML reads the key with its quote
(PyYAML gives {'name"': 'Build', 'run': 'make'}), or refuses the file
(`- "run: make`), and GitHub refuses a workflow with a step key outside
the step schema: a HOST_TESTED row passed the lint for a workflow that
runs nothing. The HOST_TESTED comment, the workflow_steps docstring and
the Test page state the rule for a step's key, and every step's first
key is on its dash line. No workflow of the tree has such a key.
workflow_steps now reads the first key of every list item as it reads a
step's other keys: the dash and the spaces after it become spaces, and
a key with one stray quote there makes every step of the workflow carry
"unread", which _on_every_run refuses. The rule also takes in a list
item outside the steps (a matrix include entry): that only lists more.
The docstring says so.
Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s
tests -v` in forgetest): test_manifest.py runs 39 tests, OK, on Windows
with python 3.14 and on Ubuntu 24.04 with python 3.12.
test_a_step_counts_only_when_every_run_of_the_workflow_runs_it_and_its_failure_fails_the_run
gains a step `- run": make` before a step that runs the test: no step
counts. Negative control on a scratch copy: manifest.py as it was fails
that case (True is not false). A probe of workflow_runs: `- name":
Build` with `run: make` under it, and `- "run: make`, each before a
step that runs the test, count as no step, as a stray quote on a
step's second key already did. The 17 workflows of the tree's
repositories (211 steps) read to the same events and steps before and
after the change. The coverage lint on the tree manifest from the
recipe pins reports 0 uncovered paths, 0 host-tested row problems, 43
tests. pyflakes reports no undefined name.
Acceptance: none. forgetest is dev-only and in no fingerprint (the
source_sha of 43 of 43 tests is unchanged); the lint runs in CI, not in
a campaign, and tests/ are in no fingerprint.
|
||
|
|
609ae561bf |
forgetest: forgectrl's test scratch trees leave the repository, and the two still written are ignored
forgetest/cool-flow-test/, forgetest/super-test/ and
forgetest/cool-fanwrite-test/ (46 files) are the scratch trees that
forgectrl's cool_flow_test and super_test, and the fan-write test that
cool_flow_test took in, write in the directory they run from: fake
sysfs attributes, a cooling.state verdict, an empty dev/glowforge.
|
||
|
|
c849603942 |
bench README: the lifecycle harness row links the Test page plainly, as the bench page does
The row said that what laser_lifecycle_test.py walks is on the site, Developers, "Testing". The Test page lists four of the fifteen sessions main() runs, by choice: the harness header is the full list. And the page is "Test" in the site's navigation, in its title and in the bench page's own link; the site has no page called "Testing". The row now says what the harness is and links the Test page by its title, as the bench page of the site does. Proof: README text only, no behavior change. The row names no session and makes no claim about the page's list; its link text is the page's title (mkdocs.yml: "Test: developers/testing.md"), and its URL does not change. Acceptance: none. **/*.md is outside every fingerprint (NON_BEHAVIORAL in manifest.py). |
||
|
|
6473e7d9ae |
forgetest-ci: the undefined-name step prints why it failed, and fails a run SIGINT stopped
The comment over the step said that a signal fails it. pyflakes ends a
SIGINT and a SIGPIPE itself, with status 1 ("... stopped" for SIGINT),
and status 1 goes on to the grep, so a run SIGINT stopped mid-tree
passed the step with nothing judged. And every failure the step gained
for a status other than 0 or 1 left no cause in the step log: the
group's 2>&1 also captures bash's own notice of a signal ("Killed"),
and nothing printed pyflakes.txt, so a usage error or a killed run
showed only the pyflakes version line before the failure.
The capture is printed when the pyflakes group fails, and the grep
also takes the "... stopped" line. The comment says what fails the
step: the grep's lines, a SIGINT stop, any status other than 0 or 1,
and a capture that cannot be written; a SIGPIPE ends with status 1 and
no line, and a write to the capture file raises none.
Proof: the step body, read with PyYAML from this workflow and, as the
control, from the workflow before this change, run under bash -e over
the forgetest tree on Ubuntu 24.04 with python 3.12 and pyflakes 4.0.3
(the install line a no-op: the interpreter on PATH is a shim to a
venv). The tree as it is exits 0 with both bodies, and an undefined
name planted exits 1 with its line in the log. A run that exits 2 and
a run killed by SIGKILL while it blocks on a FIFO exit 1 with both
bodies; the step log of the former body holds only the version line,
and this one prints "unrecognized arguments" and the "Killed" notice.
A run stopped by SIGINT exits 0 with the former body and 1 with this
one, its "... stopped" line in the log. A working directory that
cannot be written exits 1 with both, its "Permission denied" in the
log.
Acceptance: none; CI only. .github/ is outside meta-forgefirm and
every covers map.
|
||
|
|
fc6b7797af |
forgetest: the shared judges' comments say why puls.py is one
The comment over SHARED_JUDGES in catalog.py said that every test runs through all four judges without importing them, puls.py among them, and the comment of test_a_shared_judge_edit_moves_every_test said the same. No test runs puls.py but the three that print the dark job (cloud.lid-interlock-abort, cloud.oversize-stream and exthost.armed-freeze, through offline_job), and suite/cloud.py imports it with `from .. import puls`: it is a judge because the sibling walk does not follow that import, as the Acceptance page of the site says. Both comments now say that every test runs through runner.py, hw.py and baseline.py with no import to follow, and that puls.py comes in by a package import the walk does not follow. Comments only: SHARED_JUDGES does not change. Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s tests -v` in forgetest): test_manifest.py runs 39 tests, OK, on Windows with python 3.14 and on Ubuntu 24.04 with python 3.12. The only import of puls in the package is suite/cloud.py's (grep). pyflakes reports no undefined name. Acceptance: none. catalog.py is the registry and in no fingerprint, and tests/ are in none (source_sha of 43 of 43 unchanged). |
||
|
|
a10fd7782d |
forgetest: the hash cases hold what a test's own import imports in turn, and the guard refuses an absolute package import
No case held the step of implementation_sha that adds what a test's own import imports in turn (`sibling_imports(sib, sibs)`), nor the recursion in sibling_imports itself: the judge.py of test_a_sibling_imported_inside_a_function_moves_the_tests_that_run_it imports nothing. Either line deleted kept test_manifest.py green, while on the real catalog the first drops motion.py's shared text from cloud.mode-switch's hash and setup.py's from motion.release's. The guard on the suite's package imports read the relative spelling alone: a `from forgetest import server`, an `import forgetest.server`, or a `from forgetest.suite.setup import ...` in a suite module passed it, and the sibling walk does not follow any of them, so the module they name joined no hash. The case's judge.py now imports limit.py, which imports base.py: an edit to either moves m.two and not m.one. The guard refuses any import of the package by its absolute name in a suite module, at any depth. No suite module imports the package that way. catalog.py does not change. Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s tests -v` in forgetest): test_manifest.py runs 39 tests, OK, on Windows with python 3.14 and on Ubuntu 24.04 with python 3.12. Negative controls on scratch copies: catalog.py without `sibling_imports(sib, sibs)` in implementation_sha fails the case at the limit.py edit; the walk without its recursion fails it at the base.py edit; in suite/forgectrl.py, a `from forgetest import server` and an `import forgetest.server` inside a function, and forgectrl.auth's import of setup.py spelled `from forgetest.suite.setup import request, decode`, each fail the guard. pyflakes reports no undefined name. Acceptance: none. tests/ are in no fingerprint (source_sha of 43 of 43 unchanged). |
||
|
|
2af9b0277f |
forgetest: the lint's docstrings and its row reason say what it reads, not what a workflow runs on
host_row_problems' docstring gave the reason a row is listed as "a
dispatch, a call or a schedule runs on no change". A workflow on
workflow_call alone runs on every run of the workflow that calls it
(forgeext's package.yml is one); the lint lists it because it does not
follow a call to its caller. The reason it printed, "<ci> runs on no
push or pull request", was false the same way for such a workflow, and
for a flow mapping (`on: {push: ...}`) that names push. The docstrings
of _on_every_run and workflow_runs said that a step under an `if:`
other than true does not run on every run, where the lint evaluates no
condition: a step under `always()` or `success()` runs on every run and
still counts as no step. The Test page of the site says each of these
as the lint applies it.
The docstrings now say what the lint reads: a row is listed when the
workflow's own `on:` names neither push nor pull_request as the lint
reads it (a dispatch, a call or a schedule alone, a call not followed
to its caller, a flow mapping), and a step with a condition counts as
no step because the lint evaluates none. The row's reason reads "<ci>:
the lint reads no push or pull_request in its own on:". The comments
of the two cases that hold these rules say the same. No rule changes:
the same rows are listed, for the same workflows.
Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s
tests -v` in forgetest): test_manifest.py runs 39 tests, OK, on Windows
with python 3.14 and on Ubuntu 24.04 with python 3.12, with
test_the_workflow_has_to_run_the_host_test asserting the new reason. A
probe of host_row_problems: a workflow on workflow_call alone and one
whose flow-mapping `on:` names push are each listed with the new
reason, and a plain `on: [push]` one is not. The coverage lint on the
tree manifest from the recipe pins reports 0 uncovered paths, 0
host-tested row problems, 43 tests. pyflakes reports no undefined name.
Acceptance: none. forgetest is dev-only and in no fingerprint (the
source_sha of 43 of 43 tests is unchanged); the lint runs in CI, not in
a campaign, and tests/ are in no fingerprint.
|
||
|
|
c717ecbcce |
forgetest: the lint reads a quoted key only in a matched pair, and a key with one stray quote runs nothing
_KEY_RX took an opening quote and a closing quote each on its own, so a key with one stray quote read as the bare key: `push":` and `pull_request':` counted as push and pull_request, where YAML reads the keys `push"` and `pull_request'`, which are no events; a step's `run": ...` counted as its run command, and `uses": actions/checkout@v4` as a checkout. A HOST_TESTED row whose workflow was spelled so passed the lint, while that workflow runs on no push and runs no such step. _KEY_RX now reads a quote only as a matched pair. A key with one stray quote is no key the lint reads, and where the lint reads keys it counts as running nothing: an event key spelled so makes the `on:` name no event, and one at the top level, at a job's name or key, or at a step's key below its dash line makes every step of the workflow count as no step (each carries "unread", which _on_every_run refuses). So a step's `if": false` still keeps the step from counting, as it did under the loose reading, and under a `jobs":` top level, where a job's `if: false` goes unread, no step counts either. The HOST_TESTED comment says so. The lint only lists more: a workflow with no such key reads as before. Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s tests -v` in forgetest): test_manifest.py runs 39 tests, OK, on Windows with python 3.14 and on Ubuntu 24.04 with python 3.12. test_the_events_a_workflow_runs_on gains an `on:` with `push":`, and one with `push:` and `'pull_request:`, each naming no event, and test_a_step_counts_only_when_every_run_of_the_workflow_runs_it_and_its_failure_fails_the_run gains a step with `if": false`, a step with `run": ./build/tray_test`, a job with `if": false` and a `jobs":` top level, none of them running the test. Negative controls on scratch copies: _KEY_RX as it was fails the `run":` step and the `push":` event; the matched pairs with no stray-quote rule fail the `if": false` step and the second `on:`; the rule at a step's keys alone fails the job's `if": false`. The 17 workflows of the tree's repositories (211 steps) read to the same events and steps before and after the change. The coverage lint on the tree manifest from the recipe pins reports 0 uncovered paths, 0 host-tested row problems, 43 tests. pyflakes reports no undefined name. Acceptance: none. forgetest is dev-only and in no fingerprint (the source_sha of 43 of 43 tests is unchanged); the lint runs in CI, not in a campaign, and tests/ are in no fingerprint. |
||
|
|
49f087f376 |
forgetest: a failed press is recorded as the fixture's only when its request went out
_press_taken recorded the press as the fixture's for every FixtureError
that arrived with the button out. That included failures in which the
box made no press for the request: its refusal (a 401, or the 409 of a
jumper pulled after the probe), a failed mDNS lookup, a refused
connect. When the person at the machine pressed meanwhile, or the arm
wait ended for another reason, the evidence and the exported artifact
gave the box a press it never made, and press_now returned True.
FixtureError now carries `sent`: True when the failure came after the
request went out (a timeout or a reset once the request was sent,
which urllib raises bare; a failed connect or send is a URLError), so
the box may have pressed. The one retry after a fresh lookup keeps it,
also when the lookup itself fails. A refusal and a failed lookup leave
it False. With the button out, _press_taken still puts up no notice,
and it records the press as the fixture's only for a sent error;
otherwise the record stays the operator's, and press_now returns
whether the fixture pressed. No judge reads `by`, and no energy path
changes.
Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s
tests -v` in forgetest): test_fixture.py runs 22 tests, OK on Ubuntu
24.04 with python 3.12, and on Windows with python 3.14 with
test_ready_reads_a_short_press_as_an_edge skipped (no os.mkfifo).
test_a_failure_after_the_request_went_out_says_so drives the real
client against the fake box: a press whose reply comes after the
client's timeout is sent, with one press, also when the refresh lookup
after it finds nothing; the jumper-out refusal, a failed lookup and a
refused connect are not sent, with no press.
test_the_arm_notice_comes_down_when_the_press_is_taken gains the leg
where the box refuses while the button goes out: press_now returns
False with no notice and the record the operator's, and arm_press
leaves the same. test_server.py (10), test_operator.py (17),
test_queue.py (15), test_mode.py (9), test_setup_dark.py (21) and
test_setup_burns.py (24) pass on both. Negative controls on scratch
copies: _press_taken giving every failure to the box fails the refusal
leg (press_now True); `sent` never set fails the first client leg;
`sent` set for a URLError too fails the refused-connect leg; the retry
that drops the flag fails the retry leg. pyflakes reports no undefined
name.
Acceptance: every catalog test's fingerprint moves: runner.py is a
shared judge, in every implementation hash (43 of 43 move); fixture.py
is in none. Against the campaign this series already needs, that adds
no run:
|
||
|
|
e046c534c2 |
setup.focus: the tube-current case plants a reading at HV_ON, so it holds the threshold
test_a_burn_whose_tube_current_the_test_never_saw_rise_fails planted pic/hv_current at 0. That proved the check on the test's own reads exists, but not its threshold: the check written as `> 0`, or as `>= HV_ON`, passed every case. The case now plants HV_ON itself (30, which is not over 30), so with the 431 the passing cases plant, the threshold is held between 30 and 430. setup_burns.py does not change. Proof: tests/test_setup_burns.py, the Unit tests step of forgetest-ci.yml (python -B -m unittest discover -s tests), 24 tests OK on Windows with python 3.14 and on Ubuntu 24.04 with python 3.12. Negative controls on scratch copies: the check as `> 0`, as `>= HV_ON`, and as no check at all each fail the case (Failed not raised); the case as it was (a plant of 0) passes against the `> 0` check. pyflakes reports no undefined name. Acceptance: none. tests/ are in no fingerprint (source_sha of 43 of 43 unchanged). |
||
|
|
36c1b75bb8 |
setup.focus: the head's place judged against the stops its manual home declared, read from the home anchor
head_at_home placed the gantry stops at minus manual_home_x and manual_home_y as the settings read at the burn. A manual home reads them once: it declares the head at the stops at minus the offsets, and with an offset it moves the head to X0 Y0, that far in front of them. A write of the offsets after the home restarts no controller and keeps the X/Y reference. So a home made with 5 and 3, the offsets then cleared, had the head at X0 Y0 judged home 5 and 3 mm in front of the stops, and the burn started: the false PASS |
||
|
|
ce73dbb052 |
forgetest: a check run_check stops following at its timeout is aborted before the test fails
run_check judged its timeout with ctx.check, which raises, ahead of the
abort meant for that case, so the abort never ran: a check still
running at the end of its follow was left running behind the FAIL. For
setup.focus that is a live burn wizard. Its put-back waits 120 s for
the burn to end and then skips the controller restart, and forgectrl's
own bound on a live run (1500 s) outlasts the test's 900 s follow. The
same driver serves the setup.check-* tests,
setup.cloud-header-capture and motion.release.
The abort now goes out before the timeout is judged, and the
unreachable return after it is gone. The change only ends a check the
test started itself: it adds no energy and no motion.
Proof: tests/test_setup_dark.py, the Unit tests step of forgetest-ci.yml
(python -B -m unittest discover -s tests), 21 tests OK on Windows with
python 3.14 and on Ubuntu 24.04 with python 3.12:
test_a_check_that_does_not_end_in_time_is_aborted_before_the_test_fails
(a lens.travel check that never ends, followed for 0.2 s: Failed, and
/wiz/lens.travel/abort posted). test_setup_burns.py (24) and
test_setup_suite.py (15) pass. Negative control on a scratch copy: the
timeout judged before the abort, as it was, fails the case (only
/wiz/lens.travel/start posted). setup.focus run whole against
test_setup_burns.py's scripted daemon with a burn that never ends (the
follow cut to 0.5 s, the press prompt held open): before, Failed with no
abort, the wizard still running and the restart skipped ("the burn is
still ending"); after, Failed with laser.focus aborted, the wizard
stopped, and the controller stopped and started (200, 200). pyflakes
reports no undefined name.
Acceptance: the shared text of setup_dark.py moves the fingerprints of
its five tests (setup.check-switches, setup.check-airflow,
setup.check-motion, setup.check-lens, setup.cloud-header-capture), of
setup.focus (setup_burns.py imports it at its top level) and of
motion.release (its own function imports run_check); the other 36 do
not move (source_sha compared before and after). Against the campaign
this series already needs, that adds no run:
|
||
|
|
7b0f065a3f |
laser.emission-witness: the head is placed back only by a sample that read the window open
rapid_place searched the samples up to the first disarmed one, that one
included, and sample_to_disarm read each sample's X after its armed
flag. So the first sample to read the window closed could place the
head back with an X read after the close: a window that closed on the
return leg, up to about one sampling loop before the head was back,
placed the rapid inside the window with a disarm time of 0, and the
`0 <=` on that time could not fail.
rapid_place now searches the samples before the first disarmed one
alone, and sample_to_disarm reads the controller's report (its state
and X) before the sample, so a sample that reads the window open read
its X with the window open. The head stands back for 1 s before the M2,
not 0.5 s: the placement needs a sample whose X and armed flag both
come between the head's return and the close, and a slow sampling loop
needs that time. The judge only gets stricter. No energy or motion path
changes: the longer dwell runs dark after the M5 with the window open,
as the shorter one did.
Proof: tests/test_laser_dwell.py, the Unit tests step of forgetest-ci.yml
(python -B -m unittest discover -s tests), 13 tests OK on Windows with
python 3.14 and on Ubuntu 24.04 with python 3.12:
test_a_window_closed_before_the_head_is_back_does_not_place_the_rapid
gains the close at the first sample with the head back (2, None, 6),
and test_the_sampling_runs_on_past_the_close_and_ends_where_it_stopped
holds the order (each sample's X read before its armed flag).
test_cooling_suite.py (3) passes. Negative controls on scratch copies:
the search with the first disarmed sample included fails the first
case with (2, 6, 6); the report read after the sample fails the second.
emission_witness run whole on a fake clock with only its I/O stubbed
(the review's sweep: the armed flag served mid-GET, X read mid status
report, 40 phases of the rapid against sampling loops of 0.20 to
0.64 s): a window closing 0 to 2 s before the head is back fails the
placement at every phase and every loop, and the normal close passes
at 40 of 40 phases at every loop. The code before this change passed
early closes at some phases up to about one loop early (0.20 s at a
0.20 s loop, 0.5 s at a 0.64 s loop). With the new order and the 0.5 s
dwell, the normal close passed 39 of 40 at a 0.38 s loop and 12 of 40
at a 0.64 s loop. pyflakes reports no undefined name.
Acceptance: laser.emission-witness's fingerprint moves (its own text and
laser.py's shared text), laser.armed-kill and
laser.pause-resume-lid-cancel move through the shared text, and
cooling.flow-under-load and cooling.fail-tier-stop move because they
import from .laser in their own functions; the other 38 do not move
(source_sha compared before and after). Against the campaign this
series already needs, that adds no run:
|
||
|
|
d57421c71d |
bench README: the lifecycle harness row points at the Testing page
The row for laser_lifecycle_test.py in scripts/bench/README.md credited the harness with M5/M3 persistence inside a job, the M2 close, and the arm refusal under a blocking cooling verdict. The harness runs no session for any of the three: its header gives them to laser_arm_test.c case I, laser_stream_test session K, and laser_arm_test.c cases C, A2 and A3. The row also restated a list that the site keeps (Developers, "Test"), and a README is an index card. The row now says what the harness is and points at that page, as the bench page of the site does. Proof: README text only, no behavior change. The three claims are gone from the file, and each of the four sessions the Test page lists is one main() runs. Acceptance: none. **/*.md is outside every fingerprint (NON_BEHAVIORAL in manifest.py). |
||
|
|
50c56a863d |
laser_stream_test: rule 19 names every laser session that runs with the dose curve in force
Rule 19 of the header said that every session but the rolloff reference run has laser_dose_curve = off. Beside that run, four laser sessions run with the curve in force: rolloff-g1 and rolloff-g2 (rule 20) and rolloff-two (rule 23) under DENSITY_CONF_CURVED, which carries no curve key, and jog-dark (rule 22), which runs with no configuration at all, so the controller's curve_default applies. Rule 20 itself says "under M4 with the curve in force". Rule 19 now says that every laser session but the rolloff ones (rules 20 and 23) and the jog run (rule 22) runs with laser_dose_curve = off. The z session carries no laser key and fires nothing, so "laser session" leaves it out. Proof: header text only, no behavior change. python3 -B -m py_compile and pyflakes are clean on scripts/bench/laser_stream_test.py, and the harness passes whole, every session, against the native null-sink build of the driver at badc832 (-DGFMCODE_WAIT_S=5.0). The harness runs in grblHAL-glowforge CI (the step "Laser stream emission rules", python3 forgefirm/scripts/bench/laser_stream_test.py build/grblHAL_glowforge). Acceptance: none; the header docstring of a harness outside every covers map. |
||
|
|
2172ae4101 |
setup.focus: the head's place judged against the gantry stops, at minus the manual home offsets when they are set
head_at_home judged a head referenced by a manual home at X0 Y0, on
the premise that a manual home declares X0 Y0 at the gantry stops. That
holds only while manual_home_x and manual_home_y are blank or 0. With an
offset, the manual provider declares the head at the stops at minus the
offsets and then jogs it to X0 Y0, that far in front of them
(glowforge_homing.c manual_home; the Homing pages of the docs), and
/status reads pos X0 Y0 there. The wizard draws every card from the
head's home against the stops (wizlive.c head_home; Setup, "The head at
home"), and the test answers HOME to that prompt. So with an offset the
judge recorded judged True and "the head is home on record" for a head
the offsets in front of the stops, the burn ran from there, and its box
landed that much farther from the stops. The failure message (home it
there) rebuilt the same state.
The sources put the home of the burn at the stops, and the stops at
minus the offsets in the coordinates /status reports, so the judge now
judges the head there. head_at_home reads the two offsets from
/settings (blank is 0; a value that is not a number of 0 or more fails
the test before any start), records the stops it judges against in
head_at_start, and judges pos against them within HOME_TOL_MM. With no
offset that is X0 Y0, the rule as it was. With an offset, a head that a
manual home left at X0 Y0 fails before any wizard start, with no
controller lever touched, and the message names the offsets, where
they put the stops, and the remedy: release the motors and push the
head against the stops (the head's place then rests on the operator's
word, as for any unreferenced head, once the release ends with
Energize motors). Judging at X0 Y0 under an offset
would affirm a place the wizard does not ask for, and recording the
read without judging it would let the test answer HOME with a read in
hand that puts the head away from the stops; neither is taken. The
offsets are read as they stand at the burn: /status does not report
the offsets the home used, and the home anchor, which holds the stops
that home declared, is not read. The description and the comments say
what is judged.
Proof: tests/test_setup_burns.py, the Unit tests step of
forgetest-ci.yml (python -B -m unittest discover -s tests), 24 tests OK
on Windows with python 3.14 and on Ubuntu 24.04 with python 3.12:
test_a_manual_home_offset_puts_the_stops_off_x0_y0 (manual_home_x 5 and
manual_home_y 3, homed_axes 7, home_source manual: at X0 Y0 the test
fails before any start, the stops on record at X-5 Y-3, no /controller/
lever posted, the settings as found; at X-5 Y-3 the burn runs, judged
True; manual_home_x "five" fails before any start), and the lit burn's
record carries the stops at X0 Y0. Negative controls on scratch copies:
setup_burns.py as it was, and the offsets taken as 0, each fail the new
case at its first run (the burn starts). pyflakes reports no undefined
name.
Acceptance: setup.focus's fingerprint moves (its own text and
setup_burns.py's shared text); no other test moves (source_sha compared
before and after). Against the campaign this series already needs, that
adds no run:
|
||
|
|
9451587728 |
setup.focus: the test's own sysfs reads go on record however the burn ends, and their tube-current check has a case
emission_seen, the two peaks the test reads from sysfs at every poll
and the count of polls that read both, was written inside emission_ok
after the three checks of the burn's result. A burn the daemon reports
dark fails at the first of them (Context.check raises), and a burn that
ends in a wizard error, a timeout or an abort never reaches emission_ok.
On each of those paths the record kept the daemon's word alone, where
the test's own reading is the one worth comparing with it. burn() now
records emission_seen, and logs it, in the finally of its run_check, so
it is on record however the burn ends. emission_ok keeps its checks, in
their order.
The check that the tube current read over HV_ON from here had no case
that could fail it: the one negative case reads both attributes at 0
and stops at the LASER_ON check, which comes first. A new case reads
the LASER_ON count nonzero and the tube current at 0, and fails on the
tube current.
Proof: tests/test_setup_burns.py, the Unit tests step of
forgetest-ci.yml (python -B -m unittest discover -s tests), 23 tests OK
on Windows with python 3.14 and on Ubuntu 24.04 with python 3.12, with
tests/test_setup_dark.py (20 OK):
test_a_burn_whose_tube_current_the_test_never_saw_rise_fails
(cnc/laser_on_sampled 118, pic/hv_current 0: Failed "the tube current
never read over 30 from here", the settings as found);
test_a_dark_burn_fails_and_still_restores finds emission_seen at 118
and 431 beside the dark result; the aborted burn's record carries
emission_seen too. run_test keeps the Run of a test that fails.
Negative controls on scratch copies: the tube-current check made
vacuous (True or ...) fails the new case (Failed not raised);
setup_burns.py as it was fails the dark case (no emission_seen) and the
aborted case. pyflakes reports no undefined name.
Acceptance: setup.focus's fingerprint moves (setup_burns.py's shared
text); no other test moves (source_sha compared before and after).
Against the campaign this series already needs, that adds no run:
|
||
|
|
bed46e961e |
laser.emission-witness: the read rule ends where the sampling stopped, and the rapid is placed inside the window
The dark stretch after the M5 runs on past the close, and three of its
judgments did not hold with it.
The read rule took the last sample served as the trail's last edge, and
sample() returns None for a request the daemon does not answer. So an
outage after the served close (one request that hangs to the client's
10 s timeout is enough) left a trail that ended at the close with no
gap, and the window that latches after the close was never read: the
test passed whole with a 30 ms fire at the end of the return rapid.
The sampling could also stop as early as about 1.8 s past the close,
and with a 0.9 s unread tail allowed, the last reading could come
before the latch of the window in flight: the review's tail model
(the kernel's ~1 s window latch, the sampler's GET timing, a fire just
before the close) finds 4 runs in 20000, 21 with a wider GET spread,
where that window goes unread and the judge passes.
The placement and the disarm timing searched the whole stretch. A
window that closed before the head was back (in the dwell at +20 mm
or on the return leg, up to about 2 s early) found the head back after
the close and passed "the rapid ran inside the dark stretch", with a
negative disarm time.
The rapid's sample count in the record ran past the close while its
span stopped at the close, so the record read about 8 Hz for a 5 Hz
loop.
sample_to_disarm moves from emission_witness to module level, ends two
windows and EMISSION_READ_GAP_S past the close, and returns the time
the sampling stopped beside the samples. read_gaps takes that time as
the trail's last edge (t_end), so a tail the daemon served none of is a
gap. rapid_place judges the placement over the armed part alone, up to
the first disarmed sample, and the disarm time must be 0 or more. The
rapid block counts its samples to the close and records how long the
sampling ran past it (sampled_past_close_s), and the log line says
both. The relit, HV and read judgments still cover the whole stretch.
The description says what is judged. tests/test_laser_dwell.py drops
relit(), a copy of the judge that no change to laser.py could fail.
Every change makes the judge stricter: no energy or motion path
changes.
Proof: tests/test_laser_dwell.py, the Unit tests step of
forgetest-ci.yml (python -B -m unittest discover -s tests), 13 tests OK
on Windows with python 3.14 and on Ubuntu 24.04 with python 3.12:
test_a_tail_the_daemon_never_served_fails (every sample served read,
then nothing for 2.9 s before the sampling stopped: gap 2.9 s, the rule
fails; served through, it holds),
test_the_sampling_runs_on_past_the_close_and_ends_where_it_stopped
(sample_to_disarm on a fake clock, the daemon serving the close and
nothing after it: the sampling ends at least 2.9 s past the close and
the rule fails), and
test_a_window_closed_before_the_head_is_back_does_not_place_the_rapid
(a close after the return places out, back and the close; a close in
the dwell at +20 mm leaves back None). test_cooling_suite.py (3) and
test_setup_burns.py (22) pass. Negative controls on scratch copies:
read_gaps without the end edge fails the first two cases; the end at
two windows fails the second; sample_to_disarm that returns the samples
alone errors in the second; rapid_place over the whole stretch fails
the third. emission_witness run whole on a fake clock with only its I/O
stubbed: a window closing 0.2, 1.0, 1.9 or 2.5 s before the head is
back fails the placement (the code before this change passed the first
three), and a normal close passes with 22 samples over 4.07 s to the
close in the record (32 before). Against a simulated daemon and kernel
latch with that 30 ms fire: an outage after the served close and one
request hung to the 10 s timeout fail the read rule (gaps 3.05 s and
10.32 s; both passed before); a tail served through with no fire
passes. In the review's tail model, the end edge alone and the longer
sampling alone each leave 0 of 20000 runs with the window in flight
unread and the judge passing, in both GET spreads. pyflakes reports no
undefined name.
Acceptance: laser.emission-witness's fingerprint moves (its own text
and laser.py's shared text), laser.armed-kill and
laser.pause-resume-lid-cancel move through the shared text, and
cooling.flow-under-load and cooling.fail-tier-stop move because they
import from .laser in their own functions; the other 38 do not move
(source_sha compared before and after). Against the campaign this
series already needs, that adds no run:
|
||
|
|
41ca7fab82 |
forgetest-ci: the coverage lint step fails when the lint fails, not only when tee does
The "Coverage lint (enforced)" step pipes the lint into tee for the step summary. A step with no shell: runs under bash -e with no pipefail, so the status of the step was tee's. An uncovered path, an empty covers entry, a host-tested row problem (the lint exits 1) and a manifest the lint cannot load (exit 2) each passed the job: the first three after they printed the report, the last after one error line on stderr and an empty step summary. The header of the workflow and the Testing page say that an uncovered path fails the job, and this step is the only place that runs the lint with --enforce. The run block now starts with `set -o pipefail`, so the status of the step is the lint's. The tee to the step summary stays. A comment above the block says why. Proof: the step body, read with PyYAML from this workflow and, as the control, from the workflow before this change, run under bash -e from forgefirm/forgetest of a copy of the tree on Ubuntu 24.04 with python 3.12, GITHUB_STEP_SUMMARY a file. The tree manifest from the recipe pins: both bodies exit 0 and write the summary (0 uncovered paths, 0 host-tested row problems, 43 tests). The same manifest with one uncovered forgectrl path planted: the lint reports 1 uncovered path, the former body exits 0, this one exits 1, and both write the summary. The manifest missing: the former body exits 0, this one exits 2. Acceptance: none; CI only. .github/ is outside meta-forgefirm and every covers map. |
||
|
|
eb684d366e |
forgetest-ci: the undefined-name step fails on a pyflakes status other than 0 or 1
The step captured the pyflakes run with `|| true`, because pyflakes
exits 1 for any message, and judged the capture alone: "undefined
name" or "Traceback". A run that ended abnormally with no traceback
passed the step with nothing checked: a run killed by a signal, a usage
error (exit 2), or a working directory where the capture cannot be
written (grep then reads no file, and the `if` is false).
The capture is now `{ python -B -m pyflakes forgetest tests || test $?
-eq 1; } > pyflakes.txt 2>&1`. The two normal statuses of pyflakes, 0
and 1, go on to the grep; any other status fails the step under
bash -e, and so does a redirect that fails. The grep stays the gate
for an undefined name and for a Traceback. A comment above the run
block says so.
Proof: the step body, read from this workflow and, as the control,
from the workflow before this change, run under bash -e over the
forgetest tree on Ubuntu 24.04 with python 3.12 and pyflakes 4.0.3
(the install line left out: the interpreter on PATH is a shim or a
venv). Both bodies: the tree as it is exits 0; an undefined name
planted, pyflakes missing, and a pyflakes that raises each exit 1. A
pyflakes killed by SIGKILL, one ended by SIGTERM, one that exits 2,
and a working directory that cannot be written: the former body exits
0 each time, this one exits 1. PyYAML reads the run text of the step
as written.
Acceptance: none; CI only. .github/ is outside meta-forgefirm and
every covers map.
|
||
|
|
9ea9c5d10f |
forgefirm-image: the pack-licenses test says what tar does with a member it cannot read
The docstring of the case that holds the refusal said that the
manifest check alone passes a bundle truncated after the unreadable
member, and a comment on the member's directory said that it sorts
after license.manifest. Neither is the mechanism. tar reports an error
that it cannot open the member, skips it, writes every other member
and the end of the archive, and exits nonzero only at its end, a
status the former pipeline lost. Where the member sorts does not
matter. The docstring now says that tar skips the member and the
manifest check alone passes the bundle without it, and the comment is
gone.
Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s
tests -v` in forgetest), on Ubuntu 24.04 with python 3.12 as an
unprivileged user: test_image.py's PackLicensesTests, 5 tests OK. With
the pack script as it was before
|
||
|
|
5d26818946 |
forgetest: a press the fixture made and reported failed leaves no notice standing
press_now and arm_press hand the arm press to the operator when the
bench actuator reports that the press failed. When the box makes its
200 ms press and its reply is lost, the client raises that failure at
its 3 s timeout or later (with ip set in the config it does not
retry), and by then the controller has taken the press and put the
button out. The fallback put the notice up all the same and started
the watch, which waits for the button to light before it waits for it
to go out. The button does not light again, so "Press it now" stood
through the job and asked for a second press, and a press during the
job pauses it.
Both fallbacks now record the fixture's error and then read the
button. Out: the press was taken, it is recorded as the fixture's, and
no notice goes up. Lit, or unreadable: the notice goes up with its
watch, as before. Nothing presses for the operator, and no judge or
energy path changes.
Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s
tests -v` in forgetest), on Ubuntu 24.04 with python 3.12 and on
Windows with python 3.14: test_fixture.py's
test_the_arm_notice_comes_down_when_the_press_is_taken gains the leg
where the box puts the button out and then reports a failure: press_now
returns True with no notice and the press recorded as the fixture's,
and arm_press leaves no notice and the same record. The file's 21
tests OK on Ubuntu, and on Windows with
test_ready_reads_a_short_press_as_an_edge skipped (no os.mkfifo).
test_setup_burns.py (22), test_setup_dark.py (20), test_server.py
(10), test_operator.py (17), test_queue.py (15) and test_mode.py (9)
OK on Ubuntu. Negative controls on scratch copies: the leg
against the runner as it was fails at press_now (False, the notice
standing); with arm_press's fallback alone as it was, the leg fails at
its record ('operator', the notice up). pyflakes reports no undefined
name.
Acceptance: every catalog test's fingerprint moves: runner.py is a
shared judge, in every implementation hash (43 of 43 move). Against
the campaign this series already needs, that adds no run:
|
||
|
|
8d16b93e86 |
forgetest: the shared judges and a test's own imports reach the hash the gate reads
The sibling walk read the top-level statements of a module alone. Five
tests import their judging helpers inside their own functions, so that
no other test of the module moves with them: cloud.mode-switch
(homeoff.py: judge_whole_motions, camera_home_return),
cooling.flow-under-load and cooling.fail-tier-stop (laser.py's sample
and run_and_sample, motion.py's log readers), forgectrl.auth (setup.py's
request and decode) and motion.release (setup_dark.py's run_check).
None of those helpers was in the hash of its test, and homeoff.py was
in no hash at all: an edit to judge_whole_motions moved 0 of 43 tests,
and an edit to laser.sample moved the three laser.* tests and neither
cooling test, so an inherited PASS survived a change to its own judge.
The guard that every package module a suite module imports is a listed
judge read the top level alone too, and no unit test held the
production path: Test.source_sha, which the fingerprint, the campaign
and the gate read, could drop the shared judges with CI green.
implementation_sha now adds to the hash of a test the suite modules its
own @test function imports, at any depth, and what they import in turn;
no other test of the module takes them. The shared text of a module
gives the modules it imports at any depth too: a helper that imports
inside its body brings that module into every test of the module, as
the helper itself does. _test_defs finds the @test functions of a
module for module_parts and for both walks. The guard walks the whole
tree of each suite module, so a package import inside a function or a
block is held to the list as well. test_a_shared_judge_edit_moves_every_test
holds the production path on a suite test: its source_sha equals
implementation_sha with SHARED_JUDGES and differs from it with none.
Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s
tests -v` in forgetest): test_manifest.py runs 39 tests, OK, on Windows
with python 3.14 and on Ubuntu 24.04 with python 3.12, with
test_a_sibling_imported_inside_a_function_moves_the_tests_that_run_it
(a judge imported in the body of one test moves that test and not the
other; imported in the body of a helper, it moves both), the
source_sha assertions, and the guard on the whole tree; test_artifact.py,
test_campaign.py, test_redact.py and test_responsiveness.py pass.
Negative controls on scratch copies: implementation_sha's default
taking no judges fails the source_sha assertion, and so does
Test.source_sha passing judges=(); the new case fails against
catalog.py as it was, and against a shared-text walk of top-level
statements alone; a `from .. import server` inside a function of
suite/forgectrl.py, or in a try block at its top level, fails the
guard, and the function-local one passes the guard that reads the top
level alone. On the real catalog: an edit to judge_whole_motions moves
cloud.mode-switch alone (0 tests before); an edit to laser.sample moves
the three laser.* tests and both cooling tests (the three alone
before); an edit to run_check moves the six setup.* tests and
motion.release (the six alone before); an edit to setup.request moves
forgectrl.auth and motion.release (motion.release through setup_dark.py,
which imports setup.py) beside the 8 it moved before; an edit to
hw.sysfs_int
moves 43 of 43, as before. The coverage lint on the tree manifest
from the recipe pins reports 0 uncovered paths, 0 host-tested row
problems, 43 tests. pyflakes reports no undefined name.
Acceptance: cloud.mode-switch, cooling.flow-under-load,
cooling.fail-tier-stop, forgectrl.auth and motion.release: the
implementation hash of each moves once, its own imports joining it; the
other 38 do not move (source_sha compared before and after). Against
the campaign this series already needs, that adds no run:
|
||
|
|
5bcc536853 |
forgetest: the lint's step parser keeps a step's real key column and reads a quoted key
workflow_steps rebuilt the first line of a step with its first key two columns past the dash, whatever followed the dash. A step written with more than one space after its dash (`- run: ./build/tray_test`, its `if: false` at the column of the run key) then had its first key at one column and the others at another: every key after the first was read as a child of the first and dropped, `if:`, `continue-on-error:`, `working-directory:` and `with:` among them. So such a step under `if: false` counted as running its host test, and such a checkout under a condition counted as a checkout. _KEY_RX did not read a quoted key, so a step's `"if": false` was dropped the same way. Both spellings are valid YAML that GitHub skips or tolerates. The parser now replaces the dash with a space and realigns nothing, so the key column of a step is the column its keys use. _KEY_RX reads a key in single or double quotes as the same key. The HOST_TESTED comment states the rule the lint applies: no `if:` other than `true` and no `continue-on-error:` other than `false` on the step or its job. Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s tests -v` in forgetest): test_manifest.py runs 38 tests, OK, on Windows with python 3.14 and on Ubuntu 24.04 with python 3.12, with test_a_step_counts_only_when_every_run_of_the_workflow_runs_it_and_its_failure_fails_the_run extended by a step with three spaces after its dash under `if: false` and a step under a quoted `"if": false`, neither running the test. Negative controls on scratch copies: the first line rebuilt as before fails the three-space case; _KEY_RX as before fails the quoted case. The 17 workflows of the tree's repositories (205 steps) parse to the same steps and events before and after the change. The coverage lint on the tree manifest from the recipe pins reports 0 uncovered paths, 0 host-tested row problems, 43 tests. pyflakes reports no undefined name. Acceptance: none. forgetest is dev-only and in no fingerprint (the source_sha of 43 of 43 tests is unchanged); the lint runs in CI, not in a campaign, and tests/ are in no fingerprint. |
||
|
|
c0c74d1658 |
laser_lifecycle_test: rule 3 names the holders of the sender-change reset
Rule 3 of the header says a sender change closes the window and a
reconnected client must re-arm. The harness does not walk the sequence
on from there, a new sender's soft reset of the held job, as a session
of its own (commit
|
||
|
|
b2a011ac08 |
ctlport_test: the polling sender is a Grbl sender, not a product
The NewlinePoller docstring and poll_case's named one sender product as the sender that polls '?' with an end of line behind it, and quoted that product's poll rate beside the harness's own. The behavior under test is the controller port's: an empty line is not the sender speaking, a port jog is never refused for a poll, and every poll draws its one ok. Which sender polls, and how often, is not the contract; the harness's own rate (four times a second, period 0.25) is the fact that matters. Both docstrings now say a Grbl sender that polls, and the comparative rate is gone. Proof: docstrings only, no behavior change. python3 -B -m py_compile and pyflakes are clean on scripts/bench/ctlport_test.py, and the file names no product. The harness runs in grblHAL-glowforge CI (python3 forgefirm/scripts/bench/ctlport_test.py build/grblHAL_glowforge). Acceptance: none; docstrings of a harness outside every covers map. |
||
|
|
68e9d3fb8e |
laser_stream_test: the rolloff reference pins the bench-default curve's rendering of S300 end to end
Rule 19 names laser_arm_test.c case P as the proof of the dose curve's mapping. Case P calls curveComputeValue directly and reads its counts; it does not reach the wiring from the S word to the stream: spindleConfig installing the curve as the PWM compute hook, and the level the hook returns riding the dither into FIRE density. A driver that left the hook uninstalled passes case P and renders every cut through the identity mapping, a different delivered dose at every S. The rolloff reference session (rule 23) already runs one M4 line at S300 under the shipped curve and reads its cruise density, to compare against the two-cut run. It now also holds that density to the curve's own point: 30 percent light through the bench-default curve lands between its (45 percent density, 21 percent light) and (60, 37) points, 53.44 percent density, level 68 of 127, density 0.535, held to 0.02. The expectation is mirrored next to the curved configuration the way the floor is, so a change to the curve changes it. The header's rule 19 names the point. No session is added, and no session's configuration changes. Proof: the host test grblHAL-glowforge CI runs (the step "Laser stream emission rules", python3 forgefirm/scripts/bench/laser_stream_test.py build/grblHAL_glowforge) against the native null-sink build of the driver at badc832 (-DGFMCODE_WAIT_S=5.0): the rolloff reference cruise renders 0.5355 against the expected 0.5354, and the whole harness passes, every session. Negative control, through a scratch copy of the harness: the same cut with laser_dose_curve = off renders 0.3625 (the identity mapping, level 46 through the 10 percent floor), 0.17 from the point, and fails the assertion. Acceptance: none; the harness is grblHAL's CI's, and src/** of grblhal-glowforge keeps its covers. |
||
|
|
f47373a20d |
laser_stream_test: check_density holds each lit span to the move, so a level applied late shows
check_density judges a density run by what its lit spans carry: every power byte at full duty, each span's FIRE density at its level, no burst past the base period. It did not read a span's length against the move it rode. A level that reaches the stream late (the move opens dark, FIRE starts where the level is applied) renders the right density over a shorter span, and the density alone cannot see it: with FIRE blanked in the first 40 percent of the S300 move of the idle-s stream, the check passed. Rule 14 (a level set while idle survives to the next run) and rule 12 (density rung by rung) proved the density of whatever was lit, not that the whole move was lit; rules 16 and 17 keep their span length through check_cut_spans. check_density takes an optional move length in machine ticks and holds each span to 0.9 to 1.1 of it. The three density call sites pass theirs: the ladder sessions (density, floor-derived) LADDER_TICKS, from the ladder's now explicit LADDER_FEED, and idle-s IDLE_S_TICKS. Span and move are both in the mode's own ticks, so the bound holds at every microstep mode. The bound is 0.9 to 1.1, not check_cut_spans' 0.8 to 1.25, because the real spread is about 1 percent (the faintest rung, S20, opens on carried debt at 0.991 of the move; idle-s runs at 1.007) and the looser bound lets a move 15 percent dark pass. The header's rules 12 and 14 say what they prove. Proof: the host test grblHAL-glowforge CI runs (the step "Laser stream emission rules", python3 forgefirm/scripts/bench/laser_stream_test.py build/grblHAL_glowforge) against the native null-sink build of the driver at badc832 (-DGFMCODE_WAIT_S=5.0): the density, idle-s and floor-derived sessions pass the bound with span ratios 0.991 to 1.007, and the whole harness passes, every session. Negative control on the captured idle-s stream, through a scratch copy of the harness: FIRE blanked in the first 40 percent of the S300 move (span ratio 0.604) and in the first 15 percent (0.855) fail the bound, and the same streams pass with the move length omitted; a 5 percent trim (0.956) passes, so the bound is neither vacuous nor hair-trigger. Acceptance: none; the harness is grblHAL's CI's, and src/** of grblhal-glowforge keeps its covers. |
||
|
|
199282c029 |
setup.focus: the head's place read from /status and judged at X0 Y0 before the burn starts
The focus burn answers the wizard's home prompt with HOME from a dict, and the wizard takes the operator's word the same way: nothing reads where the head stands. A read at that prompt places nothing either: the wizard restarts the controller before it, and a fresh controller drops the X/Y reference. The one read with meaning comes before POST /wiz/laser.focus/start, under the campaign's own controller, when a manual home references X and Y: with manual_home_x and manual_home_y blank or 0, it is the one provider that declares X0 Y0 at the gantry stops. head_at_home(ctx), called after the ready gate and before anything starts, waits up to 10 s for a /status that serves a position (a transient miss is not a stop; ten seconds of none fails the test before any energy), records the read as head_at_start (pos, homed_axes, home_source, judged), and judges the head at X0 Y0 within HOME_TOL_MM (0.05 mm) when homed_axes carries X and Y and home_source is manual: off its home, the test fails before any wizard start, so no controller lever is touched. Unreferenced, the head's place rests on the operator's word at the ready gate, the log says so, and judged is False. The HOME answer and the steps stay; the description names the read. The strict form, a manual home before every focus burn, is a procedure the operator must add to every campaign; this change records and judges what it can read. Proof: tests/test_setup_burns.py, the Unit tests step of forgetest-ci.yml (python -B -m unittest discover -s tests), 22 tests OK on Windows and on Linux, with tests/test_setup_dark.py (20 OK) and tests/test_laser_dwell.py (10 OK). The scripted daemon's /status carries homed_axes 7 and home_source manual: at X0 Y0 the burn runs and the evidence carries the read with judged True; at X 12.5 the test fails before any start, with no burn started, no /controller/ lever posted, and the settings as found; unreferenced (homed_axes 4, home_source startup) the burn runs with judged False. pyflakes reports no undefined name. Negative control on a scratch copy: with setup_burns.py as it was before this change (the HOME answer alone), the X 12.5 case burns (Failed not raised) and the two evidence cases find no head_at_start; the three tests catch it. Acceptance: setup.focus's fingerprint moves (its own text and setup_burns.py's shared text); no other suite module imports from .setup_burns, so nothing else moves. The next campaign runs setup.focus with the head's place on record. |
||
|
|
6a4ece4b0d |
setup.focus: the test reads the LASER_ON count and the tube current from sysfs itself at every poll of the burn
The focus burn's emission was judged from the burn's result alone: the daemon's own 25 Hz reads of cnc/laser_on_sampled and pic/hv_current, reported as laser_on_samples and hv_max. The one live catalog test of the wizard's burn took the daemon under test at its word for the emission it reported, where the laser.* tests judge from the test's own /status samples. run_check takes on_poll(doc), called once per poll of /wiz/dark with the status document, a caller's own witness read beside the check's; the default keeps every positional caller as it is. Witnesses, in setup_burns, reads the two attributes from sysfs at every poll and keeps each at its peak and the count of polls that read both. burn() passes it to run_check, and emission_ok(ctx, last, seen) keeps the result's three checks and adds the test's own: at least one poll read both witnesses, the LASER_ON count read nonzero from here, and the tube current read over HV_ON (30, the daemon's own rule for a lit burn, jobstream.h JOBSTREAM_HV_ON) from here. Once the result's three checks pass, the evidence carries the two peaks and the read count as emission_seen; the two readings are not compared with each other. The beam sensor's handling is unchanged, and the description names both readings. Proof: tests/test_setup_burns.py, the Unit tests step of forgetest-ci.yml (python -B -m unittest discover -s tests), 20 tests OK on Windows and on Linux, with tests/test_setup_dark.py (20 OK) for the positional callers of run_check. BurnRunTests plants cnc/laser_on_sampled and pic/hv_current under GF_SYSFS_ROOT (the shape of a bench record), the lit burn's evidence carries the two peaks with reads over zero, and a burn whose result reports a lit burn while the two attributes read 0 from here fails on the LASER_ON count, with the setting put back and the controller restarted on it. pyflakes reports no undefined name. Negative control on a scratch copy: with setup_burns.py and setup_dark.py as they were before this change, that same case passes (Failed not raised) and the evidence has no emission_seen; the two new assertions catch both. Acceptance: setup.focus's fingerprint moves (its own text, and the shared text of setup_burns.py and setup_dark.py), and every test of setup_dark.py moves through run_check: setup.check-switches, setup.check-airflow, setup.check-motion, setup.check-lens and setup.cloud-header-capture. motion.release does not move (its run_check import is inside the function body). The next campaign runs setup.focus with both readings on the bench. |
||
|
|
802cd3af8d |
laser.emission-witness: the LASER_ON count read through the rapid, and two windows past the close
The dark stretch after the M5 judged emission from the samples whose /status carried a laser block. forgectrl leaves that block out of a served /status when cnc/laser_on_sampled or cnc/laser_pgood_sampled does not read, and sample() records the count as None, which the relit judge (any sample with emission) and the HV judge (the maximum over the readings) both pass: a stretch whose count went unread after the dark point passed as dark. The kernel latches the count once per ~1 s window and holds it through the next, so a window with no read in it is a window in which a fire is not witnessed. The stretch also ended at the first disarmed sample, before the count for the window in flight at the close had latched, so the rapid's last second was never read. read_gaps(samples, key) reports, over a trail, the samples that carry a reading, the ones that do not, and the longest stretch of seconds between two readings, with the trail's ends as edges: an unread lead or tail is a gap, and a trail with no reading is one gap, its whole span. The judge requires RAPID_MIN_READS (4) readings of the count with no gap over EMISSION_READ_GAP_S (0.9 s), inside the EMISSION_WINDOW_S (1.0 s) latch period, and the same floor of readings for the HV current. Both checks come before the relit and HV-dark checks, so those never judge an unread stretch. sample_to_disarm keeps sampling for two windows after the first disarmed sample: the count for the window in flight at the close latches up to one window later and is held through the next, and the gap rule asks for a read of it in that tail, measured to the last sample served. The timeout bounds the wait for the close alone. The close is the first disarmed sample, which the disarm timing and the span are measured to; the evidence's rapid block carries emission_reads and hv_reads, the log line says how many samples carried the count and the longest unread stretch, and the description names the rule. Proof: tests/test_laser_dwell.py RapidReadTests, the Unit tests step of forgetest-ci.yml (python -B -m unittest discover -s tests), 10 tests OK on Windows and on Linux: every sample read (gap 0.2 s), one miss (0.4 s) and a 0.8 s hole are inside the rule; a 1.2 s hole, the block gone after the dark point (1 of 25 read, gap 4.8 s), the block gone for the last 1.6 s, and a one-sample stretch fail it. pyflakes reports no undefined name. Negative control on a scratch copy: with the trail's tail dropped from the edges, the last-window case and the block-gone case report no gap and their two tests catch it; with the gap bound raised to 1.3 s, the 1.2 s hole passes the rule and its test catches it. Acceptance: laser.emission-witness's fingerprint moves (its own text and laser.py's shared text), and laser.armed-kill and laser.pause-resume-lid-cancel move through the shared text; no other suite module imports .laser at module level. laser.emission-witness is always run, and the next campaign judges the rapid stretch's read cadence on the bench (about 5 Hz in the last campaign's record). |
||
|
|
099399b58f |
forgetest-ci: the undefined-name step fails when pyflakes is missing or raises
The "No undefined names" step runs under bash -e with no pipefail, and
-e does not reach the condition of an `if`. The former body pipes the
pyflakes run into grep, so the step's status is grep's alone: with
pyflakes absent ("No module named pyflakes" on stderr, exit 1) or
crashing, grep sees no match, the `if` is false, and the step passes
with nothing checked. Its `pip install pyflakes` also names no
interpreter.
The step now installs with `python -m pip`, into the interpreter it
runs; runs `pyflakes --version`, which fails the step under -e when
the module is missing; captures the pyflakes run, stderr included, in
a file (its own exit status is 1 for any message, unused imports
included, so it cannot be the gate); and fails on "undefined name" or
on "Traceback", a crash. The grep is pinned to pyflakes' message text;
no pyflakes version is pinned.
Proof: the step body, read from this workflow, run under bash -e over
the forgetest tree on Ubuntu 24.04 with python 3.12, against the
former body as the control. pyflakes missing: the former body exits 0,
this one exits 1. pyflakes 4.0.3 present, the tree as committed: both
exit 0 (22 unused imports and 2 unused locals, no undefined name). An
undefined name planted in a scratch copy: both exit 1. A pyflakes that
raises (a shim, scratch only): the former body exits 0, this one exits
1. In CI the step's log shows the pyflakes version before the check.
Acceptance: none; CI only. .github/ is outside meta-forgefirm and
every covers map.
|
||
|
|
03146a1529 |
forgefirm-image: pack-licenses.sh refuses a license text tar cannot read
pack-licenses.sh packs the image's license tree into one tar.gz and removes the tree. It piped tar into gzip under /bin/sh (dash, no pipefail): the pipeline's status is gzip's, and tar's is lost. The check that follows proves only that the bundle holds license.manifest. So a member tar cannot read leaves a bundle without that text, the build green, and the tree removed, the only copy of the text with it. tar now runs the compressor itself (-I 'gzip -9 -n'), so its exit status covers the read and the write. A failed pack removes the partial bundle and the script refuses, with the tree left for the build log; pack_licenses in forgefirm-image.bb fails do_rootfs on the refusal. The bundle's bytes do not change: tar feeds the same stream to the same gzip, and a tree packed by both forms gives one sha256. Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s tests -v` in forgetest), on Ubuntu 24.04 with python 3.12 as an unprivileged user: test_image.py's PackLicensesTests gain test_a_license_text_tar_cannot_read_is_refused_and_the_tree_left (a text under zlib/ with mode 0: a nonzero exit, a message on stderr, no bundle, the manifest left); the class's 5 tests OK. Negative control on a scratch copy: the new case against the script without this change fails at assertRefused with an exit status of 0. At the shell, on a fixture tree with the unreadable member, the script without this change exits 0, leaves the bundle without the text and removes the tree; with it, the script exits 1, leaves no bundle and leaves the tree. On a readable tree both forms write byte-identical bundles (one sha256 for both). pyflakes reports no undefined name. Acceptance: every catalog test. pack-licenses.sh is under meta-forgefirm, whose content hash is part of the platform in every fingerprint, so the image that carries this change needs the whole catalog on the bench. |
||
|
|
852943e20c |
forgetest: the fixture's press docstring stops inviting a long press
Fixture._press said that a check that must reach the machine while the button is down asks for the longest press the box can make, and the unit test's comment repeated the invitation. That contradicts the design: no press is ever held, every reader of the button catches the default press (an edge, or a poll of 100 ms or less), and the box clamps any request into 20 to 500 ms. The docstring now says what `ms` is (the box's own 200 ms when it is not said, clamped by the box) and that no check needs a longer press. The `ms` plumbing stays: it mirrors the box's API field, and the box clamps regardless. The test's pass-through assertion stays as the client's contract that a request reaches the box as asked; the clamp is the box's, held by the fixture firmware's policy test. Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s tests -v` in forgetest), on Ubuntu 24.04 with python 3.12: the ClientTests of test_fixture.py, 6 tests, OK. A grep for "longest press" and "while the button is down" over fixture.py and test_fixture.py prints nothing. Text only, so no negative control applies. pyflakes reports no undefined name. Acceptance: none. fixture.py is a non-suite module outside the shared judges, and tests/ are in no fingerprint; no press length or path changes. |
||
|
|
a596767a9a |
forgetest: a press the fixture could not make leaves a notice that comes down at the press
press_now and arm_press hand the arm press to the operator when the
bench actuator refuses it (the box is off) or when the button never
lights for it within the arm wait, and put the notice up for a person.
Those three fallbacks raised the notice without the watch that takes
it down at the press, so the notice stood until the test's next step:
the second-press hazard the watch exists for, because a press during
the job pauses it. Each fallback now calls _clear_when_taken() after
its notice, so the notice comes down the moment the controller takes
the press, as the operator's own path does. No press, judge, or energy
path changes.
Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s
tests -v` in forgetest), on Ubuntu 24.04 with python 3.12:
test_fixture.py's test_the_arm_notice_comes_down_when_the_press_is_taken
gains the leg where the fixture cannot press: press_now's notice for a
refused press, arm_press's notice for a refused press, and arm_press's
notice for a button that never lit each come down within 2 s of the
press. Three runs OK, the file's 21 tests OK. Negative control on a
scratch copy: the leg against the runner without the three calls fails
at the first press, the notice still standing. pyflakes reports no
undefined name.
Acceptance: every catalog test's fingerprint moves: runner.py is a
shared judge, in every implementation hash (43 of 43 move). Against
the campaign this series already needs, that adds no run:
|
||
|
|
3d2f2d9ab4 |
forgetest: the arm-notice watch ends with its notice and the run
_clear_when_taken takes the operator's arm notice down the moment the
controller takes the press (the button lights for the arm wait and
goes out at the press). Its watch thread polled the button LEDs every
50 ms with the arm's timeouts as its only exits: 30 minutes for the
button to light, 90 minutes in all. Nothing in it read the run's end,
the abort event, or whether the notice it guarded still stood, so a
test that ended before the press (a FAIL on the way to the arm) left a
thread reading up to six sysfs files twenty times a second behind it.
The watch now lives only as long as the notice it guards and the run:
a notice cleared or replaced, a run that finished, or an abort ends
it. `standing()` reads both under the run's lock on every poll, and
the poll waits on the run's abort event instead of sleeping, so an
abort ends the wait at once. The cadence stays 50 ms: the notice still
drops within 50 ms of the press. The watch commands nothing; no press,
judge, or energy path changes.
Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s
tests -v` in forgetest), on Ubuntu 24.04 with python 3.12:
test_fixture.py's test_the_arm_notice_comes_down_when_the_press_is_taken
gains the leg that a run finished before the press ends the watch
within 2 s and leaves the finished run's notice as it was; the file's
21 tests pass; test_setup_burns.py, whose burns reach the watch
through press_now, passes its 19 tests and leaves no
forgetest-arm-notice thread behind (10 before this change); and
test_server.py, test_operator.py and test_queue.py pass. Negative control on a scratch
copy: the new leg against the unchanged runner fails with the
forgetest-arm-notice thread still alive after the run finished.
pyflakes reports no undefined name.
Acceptance: every catalog test's fingerprint moves: runner.py is a
shared judge, in every implementation hash (43 of 43 move). Against
the campaign this series already needs, that adds no run:
|
||
|
|
850498fb00 |
forgetest: the shared judges join every test's implementation hash
Every catalog test judges the machine through package modules it never imports: the Context a test is handed and the takeover (runner.py), the sysfs, LED, switch and HTTP readers behind it (hw.py), and the fresh-boot idle state judged around every run (baseline.py). The cloud and extension tests print the dark job of puls.py, which the suite imports at level 2, and the sibling walk follows level-1 imports only. So none of the four was in any fingerprint: a weakening of sysfs_int, button_lit, Context.check, wait_for or position_quantized moved no hash, and an inherited PASS of kernel.latch-locked-idle, laser.armed-kill or any takeover test survived it unseen by the gate. catalog.py names them in SHARED_JUDGES, with the criterion in the comment above it: a module every test runs through without importing it, or the dark job the cloud tests print. The rest of the package (the server, the page, the campaign rules, the artifact, the manifest, the registry, the bench actuator) judges nothing about the machine and stays out. shared_judge_paths() resolves the four next to the module, and implementation_sha folds their text after the module's shared text and its imported siblings. A listed file that is missing raises, never a hash computed without it. _direct_siblings stays level-1 only, and the tool version does not move: the gate judges by source_sha and the fingerprint. Every catalog test's implementation hash moves once with this change, so every recorded PASS is stale and the next campaign is a full one, 43 tests. The catalog hash does not move, so an open campaign stays open with every test required. From here on, an edit to runner.py, hw.py, baseline.py or puls.py moves every test. Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s tests -v` in forgetest): test_manifest.py runs 38 tests, OK, with test_a_shared_judge_edit_moves_every_test (an edit to a judge moves both tests of a module, a package file outside the list moves neither) and test_the_shared_judges_exist_and_take_every_package_import_of_the_suite (the four files exist next to catalog.py, and every level-2 import of a suite module is a listed judge or the registry). Negative controls on scratch copies: `judged = []` in implementation_sha fails the first case alone; "runner.py" dropped from SHARED_JUDGES fails the guard alone, naming homeoff.py's import of runner. On the real catalog the source_sha of 43 of 43 tests differs from HEAD's; with hw.sysfs_int weakened to return the latch bit on an unparsable read, 0 of 43 move at HEAD and 43 of 43 move with this change. The coverage lint on the tree manifest from the recipe pins reports 0 uncovered paths, 0 host-tested row problems, 43 tests. pyflakes reports no undefined name. Acceptance: every catalog test. The implementation hash of all 43 moves once, so the next campaign runs the whole catalog. |
||
|
|
4d8f0d2e67 |
forgetest: the lint counts a host test as run only by a step every run of the workflow runs, on push or pull request
A HOST_TESTED row names a workflow and a host test, and the coverage lint judges the row by what the workflow's steps run. The parser stored a step's `if:` and `continue-on-error:` and read neither, read no job key, and read no `on:`. So a step under `if: false`, a step whose failure the run tolerates, a job under an `if:`, and a workflow on `workflow_dispatch` alone all counted as running the test, and a checkout under a condition counted as a checkout: a disabled or tolerated host test keeps its row green and its paths covered. The lint now reads the workflow's events and the conditions of a step and of its job. `workflow_triggers` returns the events of the `on:` mapping, list or scalar. What is under an event (its branches, its paths, a dispatch's inputs) is not an event, and a flow mapping names none. A row's workflow must run on push or pull_request, or the lint reports that it runs on no push or pull request: a dispatch or a schedule alone runs on no change, and a call alone is listed as well, since the lint does not follow it to its caller. `workflow_steps` tracks the job each step is in and carries the job's `if:` and `continue-on-error:` on the step as `job`. `_on_every_run` counts a step only when neither it nor its job carries an `if:` other than the literal `true` or a `continue-on-error:` other than the literal `false`. `workflow_runs` and `checkout_path` use it, so a conditional step runs nothing and a conditional checkout is no checkout. The reason for such a step stays "does not run it". `needs:` is not read, nor are the `paths:` and `branches:` filters of an event: the lint stays a floor there. Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s tests -v` in forgetest): test_manifest.py runs 36 tests, OK, with the two new cases, test_a_step_counts_only_when_every_run_of_the_workflow_runs_it_and_its_failure_fails_the_run and test_the_events_a_workflow_runs_on, and with test_the_workflow_has_to_run_the_host_test extended by a dispatch-only workflow and a tolerated step. Negative controls on scratch copies: `_on_every_run` forced to return True fails the step case and the host-test case (failures=2); the trigger check deleted from host_row_problems fails the host-test case (failures=1). The coverage lint on the tree manifest from the recipe pins reports 0 uncovered paths, 0 host-tested row problems, 43 tests. pyflakes reports no undefined name. Acceptance: none. forgetest is dev-only and in no fingerprint; the lint runs in CI, not in a campaign. |
||
|
|
f165713d05 |
forgetest: button.c and led.c leave the host-tested rows for the covers of the tests that drive them
HOST_TESTED carried forgectrl's src/button.* and src/led.* against host_setup.sh, which presents the button as a held word in a fake switches file and no LED at all. Both are driven on the machine by catalog tests that still run: the fuse view forgectrl.auth reads is two-factor, the token and the physical button (main.c cb_fuse_identity through operator_present, button.c); the setup checks' press prompts read the button and light it through led.c, and setup.check-switches reads both back. manifest.py's own rule puts a path a catalog test exercises on the hardware in that test's covers, not in a row: in a row it is in no fingerprint, so a change to the button's poll, its debounce or the LED release moved nothing and every PASS was inherited past it. - manifest.py: the two rows go (src/hooks.h stays: main.c's hook declarations, proven through the setup routes on a host). - suite/forgectrl.py: src/button.* joins _COVERS_AUTH. - suite/setup_dark.py: src/button.* and src/led.* join DARK_COVERS, the covers of every setup check. The laser tests judge the LED by the target the controller writes (glowforge_laser.c button_led), so led.c is not theirs. Proof: the coverage lint, enforced, on the tree manifest from the current pins: 0 uncovered paths, 0 host-tested row problems, 43 tests. tests/test_manifest.py (34) and the unit suite pass. Acceptance: the fingerprints of forgectrl.auth and of every setup check move; a release campaign runs them again. |
||
|
|
80ddd53bcd |
laser_stream_test: rule 8 reads a FIRE tick's distance from a step, so a dwell burn shows under the dither
check_fire_gaps counted consecutive stepless ticks carrying FIRE and reset on any dark tick: the analog rendering of a fire state leaked into a cycle's idle pad (stepper_stream.c gf_stream_go_idle names it: a stationary dwell burn at every planner starve). Under the density model every tick goes through the dither mask, so the same leaked state renders as bursts of at most one period (80 ticks at S800) split by dark ticks, and the run never reaches the limit. With the analog churn session gone, density-churn was the only session on rule 9, and it could not see the leak it was there for. The check now reads rule 8 both ways: the run of lit stepless ticks as before, and for every FIRE tick its distance from the nearest step on either side, the one before it and the one after it. A cut's first FIRE tick leads its first step by the accel-from-rest interval (412 ticks at x32) and its last trails its last step by less, both inside the limit (1000); a FIRE tick with no step that near on either side is in a gap, and lit, however the dither spaced it. The distance is read from both sides because a stream opens dark on its lead-in pad and a cycle opens lit before its first step: depth into a stepless run alone flagged the m4 session's first lit tick, 22529 ticks of dark pad after the stream start and a few hundred before the first step. Proof: on synthetic streams, a legitimate 2253-tick dark gap between two cuts passes both readings; a continuous analog leak across it fails both; the same gap carrying a dithered leak at S800 (64 of 80 ticks lit) and at S200 (16 of 80) passed the old reading and fails the new. The whole harness passes against the native null-sink build (-DGFMCODE_WAIT_S=5.0), every session. Acceptance: none; the harness is grblHAL's CI's, and src/** of grblhal-glowforge keeps its covers. |
||
|
|
c3da9fa1bd |
Catalog: setup.account-login, motion.cancel-abort, forgectrl.settings-bounds and exthost.service leave; the rootfs lint takes image.health's file checks
The catalog is 43. Each test leaves for a host proof that its component's CI runs at the pins: - setup.account-login: the login rules are host_routes.sh's (forgectrl 6c2bb38); the render is test_image.py's new AccountRenderTests, which runs the layer's forgefirm-users.init with reload in a private mount namespace as root (a fresh /run, the scratch account files over /etc, the scratch data directory, root's, over /data): the data directory found 0700 is 0711 after the render, and as the account's own uid the home is entered and written while listing the data directory and reading the users record are EACCES. A control without the script's chmod go+x fails it (700 != 711). forgectrl.auth's cover widens to src/ui/login.*, since login.html had no other cover. - motion.cancel-abort: laser_lifecycle_test.py's new rule 17 on the null-sink controller: 0x85 stops a jog short of its target, Idle; ^X mid-move lands in Alarm with the position kept and no return to the job start; $X recovers to Idle and the next jog ships steps (counted in GFSINK_DUMP). It passes on the driver at its pin, badc832. motion.deadman requires motion.pacing in its place. - forgectrl.settings-bounds: host_routes.sh refuses the out-of-range and over-length values with the settings file byte for byte the same, and the lid lamp under GF_SYSFS_ROOT follows lid_lamp_idle (forgectrl 6c2bb38). - exthost.service: forgeext's host tests at the pinned bb75b23 - sandbox_test (the confinement from inside, the pulse device, the settings file and the setup record refused), run_test.py (the API, safe mode, the master switch, and the killed host under the init script's own wrapper), install_test.py and sdk_test.py. exthost.armed-freeze requires exthost.platform in its place. HOST_TESTED rows for each path the lint named once exthost.service left: forgectrl src/logs.* to logs_test.c and docs/advisories/extensions.md to host_setup.sh; forgeext src/api.*, caps.*, evfeed.*, holdkeep.*, httpreq.*, manifest.*, quota.*, sandbox.*, settings.*, state.* to their unit tests, netrules.* to netrules_test.py, call.* to sdk_test.py, install.*, pkg.* and index.* to install_test.py, and main.c, fflog.* and CMakeLists.txt to run_test.py. test_manifest's behavioral-path case is unaffected. The rootfs lint takes image.health's file checks (rules init-order, logging, watchdog, read-only, dhcpv6, and on a release image factory), each proved in RootfsLintTests by a drift that fails it. image.health keeps the live half: the rendered issue and motd against the running manifest, one host and no pool process, watchdog0, no userspace watchdog running, the mounts, the dev image's factory slots read-only, no DHCPv6 client running, the DHCP client's -b and parent, ifup, the getty, and IPv6. release.sh drops its PermitRootLogin and PermitEmptyPasswords greps, its ROOTFS_READ_ONLY check and its /factory fstab check, which the lint holds; it keeps the empty root shadow field, / mounted ro, and the three HostKey lines under /data. setup.py registers no test now: its helpers the deleted tests alone called go (tls_base, tcp_open, mode, press_status, wiz_summary, wait_settled, the login constants), and test_setup_suite.py keeps the cases of what remains. Fingerprints: image.health's moves (its text), every test in exthost.py, forgectrl.py and motion.py moves with its module text, and the layer content hash moves with rootfs-lint.sh: the next image inherits no campaign. Proof: the coverage lint over the tree manifest from the pins finds 0 uncovered paths and 0 host-tested row problems (43 tests); pyflakes on the touched files is clean; forgetest's unit suite under Linux passes (334), test_image.py's 28 among them. The lint passes image 20261015142442's release tree as release and its dev tree as dev, and refuses the dev tree as release (sshd, factory). laser_lifecycle_test.py passes whole on the null-sink build of badc832. |
||
|
|
56aa164708 |
Pin forgeext 0.1.4 on the pushed head
- forgeext e09f7b8 -> bb75b23 (0.1.3 -> 0.1.4): sandbox_test opens the machine's own files from inside the sandbox - /dev/glowforge, /data/forgefirm/forgefirm.conf, /data/forgefirm/setup.json - and requires EACCES of each (ENOENT is no pass; a file the host lacks is made for the run, readable by every account, and removed after). Test-only; nothing the image installs changes. Proof: forgeext's CI run on bb75b23 (38780750106) is green on a kernel with Landlock ABI 7; its log shows the three files made for the run and each open refused with EACCES (sandbox_test, 0 failures). bitbake -c fetch forgeext from this pin succeeds (2 tasks). |
||
|
|
df5ae151ab |
Pin forgeext 0.1.3 on the pushed head
- forgeext 31cdb40 -> e09f7b8 (0.1.2 -> 0.1.3): run_test's last phase runs the host under init/forgeext.init's own wrapper; a SIGKILL of the host takes its service with it at once, the wrapper starts the host again, and the service runs again under a new pid. Test-only; nothing the image installs changes. Proof: forgeext's CI run on e09f7b8 (38777839870) is green on a kernel with Landlock ABI 7, and the phase's six checks passed in it (run_test, 0 failures). bitbake -c fetch forgeext from this pin succeeds (2 tasks). |
||
|
|
10ae7bf39c |
Pin forgectrl 0.1.33 on the pushed head
- forgectrl 22a2cfc -> 6c2bb38 (0.1.32 -> 0.1.33): cam.c resolves the lid and head lamps under GF_SYSFS_ROOT (unset: /sys/glowforge/, the paths as they were), and host_routes.sh proves the lid lamp follows lid_lamp_idle: a level is written, a refused value writes nothing, a cleared key writes the default 236. Proof: forgectrl's CI run on 6c2bb38 (38769524512) is green, and host_routes.sh ran in it. bitbake -c fetch forgectrl from this pin succeeds (2 tasks). The tree manifest from the pins holds forgectrl at 6c2bb38 (229 files); the coverage lint finds no uncovered path and no row problem (47 tests); check-ui-vendor finds the shared UI files identical; test_tree_manifest passes; the gate self-check refuses the empty artifact (rc 1). |
||
|
|
c40dc6c24c |
Catalog: setup.machine-name leaves for the rootfs lint and test_image.py
Its checks, and what holds each now:
- the name is forgefirm-<the last four hex digits of the MAC>, wlan0
first and eth0 on a machine with no WiFi, and the console banner
offers the addresses and nothing else: test_image.py's
MachineNameTests run forgefirm-hostname and forgefirm-banner as the
image runs them, on a scratch /sys/class/net;
- /etc/hostname is the bind-mounted copy: image.health;
- the DHCP request sends the name, and no mDNS responder is on the
image: the rootfs lint's dhcp-name and mdns rules (
|
||
|
|
08c7df9cbb |
rootfs-lint.sh: the image content fixed at build time, checked as the last rootfs step
A new script beside the image recipe, run by both images as the last
rootfs step (ROOTFS_POSTPROCESS_COMMAND:append, after every +=, the dev
recipe's and debug-tweaks' included); a refusal is bbfatal. It checks
every rule and prints every failure before it exits. FORGEFIRM_IMAGE_KIND
(release by default, dev in forgefirm-image-dev.bb) picks the sshd policy.
The rules:
- syslog: /usr/sbin/rsyslogd is on the image, /etc/init.d/syslog starts
it and runlevel 5 starts that script; no syslogd or klogd is linked in
a bin directory and no init script runs one. This is the image half of
what logs.routing checked on the board; that test left in
|
||
|
|
61652e237d |
Catalog: exthost.motion-job leaves, after it passed on forgeext 31cdb40
exthost.motion-job stayed in |
||
|
|
c177aabefa |
Pin forgeext 0.1.2 on the pushed head
- forgeext 2d0391e -> 31cdb40 (0.1.1 -> 0.1.2): machine_host_token() opens FORGEEXT_HOST_TOKEN_FILE when it is set and not empty, and /run/forgefirm/ext-host.token otherwise (ebdbf59); the rest is host tests (run_test's sender claim, event stream, storage quota and memory.max; install_test's served page; the trimmed kit, ffx and allowlist cases). Proof: the recipe's forgeext, built from 31cdb40 (bitbake forgeext through an untracked externalsrc overlay; packaged /usr/bin/forgeext sha256 15ca6003b59f69fb9eadf686094430fbbd25e7cc177230a0417738484fe3427e), replaced /usr/bin/forgeext on the bench reference's image 20261015101725 and ran with no FORGEEXT_HOST_TOKEN_FILE in its environment. exthost.platform, exthost.service, motion.jog-roundtrip and exthost.motion-job passed on it, in campaign c-20261015103714-0c91: the package's job reached the machine as job:org.forgetest.reference through the host's credential read from the default token file, 6 lines sent and acknowledged, the head moved, the latch locked and no emission throughout. The flashed binary was put back after the run. bitbake -c fetch forgeext resolves this pin. |
||
|
|
01f59914e8 |
Catalog: forgectrl.panel-serves, setup.ssh-until-reboot, setup.first-run-flow and logs.routing leave
Each leaves for a host proof that forgectrl's build.yml runs at the pinned 22a2cfc, and that run passed (38728407038): - forgectrl.panel-serves: tests/host_routes.sh serves the panel page bundled, with the theme and the save bar and the policy frame-src 'none', and answers /status, /cam/status and the dose-curve reads. Rows: src/ui.h. - setup.ssh-until-reboot: tests/host_routes.sh, GET and POST /system/ssh with the flag in the run directory and sshd's init a stand-in told start and stop; enable=2 refused. Rows: src/paths.h. - setup.first-run-flow: tests/host_setup.sh, the first run in the page's order: the four documents accepted at their hashes, the press step breathing teal on a scratch LED tree, the press taken as the acceptance, the account and its cookie, the gate's reason after each form step, the run completed. Rows: src/advisories.*, src/button.*, src/led.*, src/hooks.h and the four first-run documents, one row each (docs/advisories/extensions.md stays with exthost.service); src/sheetid.* to tests/sheetid_test.c, the golden ids, because host_setup.sh does not read the sheet id. - logs.routing: tests/fflog_e2e.sh, the image's rsyslog.conf and the rendered rules, each logger in its own file at its level, a stray tag in system/, the kern facility in kernel/. Rows: src/fflog.*. logs.py held logs.routing alone and goes with its import. The five camera tests no longer require forgectrl.panel-serves. test_setup_suite.py keeps setup.account-login and setup.machine-name in its ids; the cases that described setup.first-run-flow (takeover, fixture, covers) go, with the cases of the helpers that left with the two tests (breathes_teal, first_run_record, sshd_policy). test_manifest's behavioral-path case runs the report with no host-tested rows: the real table now names the first-run documents, and the case is about the allowlist. exthost.motion-job stays. It is the only catalog test that grants motion.job and posts /v0/motion/job, the path that reads machine_host_token() on the default token file, and it stays the bench proof of that path until an image carries forgeext's token-file change. src/jobpost.* stays in its covers, with no row. Fingerprints: every test in forgectrl.py, setup.py and camera.py moves with the module text. The next image cannot inherit a campaign. Proof: the coverage lint over the tree manifest from the pins finds 0 uncovered paths and 0 host-tested row problems (49 tests); pyflakes on the touched files is clean; forgetest's unit suite under Linux passes. |
||
|
|
87155be433 |
Pin forgectrl 0.1.32 on the pushed head
- forgectrl c9e3347 -> 22a2cfc (0.1.31 -> 0.1.32): fflog_e2e runs its private rsyslogd from a copy of the binary, out of reach of Ubuntu's AppArmor profile for /usr/sbin/rsyslogd. The diff is tests/fflog_e2e.sh alone; nothing the image installs changes. At c9e3347 the CI run stopped at fflog_e2e, so host_routes.sh and host_setup.sh, later in the same job, never ran there. The rows the next catalog commit adds name those two scripts and fflog_e2e.sh, and the coverage lint reads build.yml at this pin, so the pin is the revision whose run executed them. Proof: forgectrl's CI run on 22a2cfc (38728407038) is green; fflog_e2e, host_routes, host_setup and jobpost_test passed in it. |
||
|
|
893602e44c |
Pin grblHAL-glowforge 0.1.21 on the pushed head
- grblHAL-glowforge b34636f -> badc832 (0.1.20 -> 0.1.21): the host tests trimmed to one proof per rule, with the cases the cut dropped back (the tray, session-budget and envelope clamps, the switch bits as numbers, an M5 then an M3 inside the armed window); a host build may shorten the M-code answer wait (-DGFMCODE_WAIT_S, CI's 5 s, for mcode_test.py's no-answer case) while the image keeps 30 s; the homing comment covers a setup step's hand-back. The driver the image builds is unchanged in behavior. It follows |
||
|
|
205f9f22f2 |
Pin forgectrl 0.1.31 and forgefirm-app 0.1.32 on the pushed heads, and meta-openglow in the kas lock
- forgectrl c274f19 -> c9e3347 (0.1.30 -> 0.1.31): setup is required up to the intake fans and recommended after, the coolant flow calibration is a required step, the TEC check gives way to the operator's answer, the head's id is head/info's "id" line, the intake fan count is decided in wizcalc, and the burns hand the lens back referenced; every start logs its firmware and slot, and every update job is logged; the beam sensor is recorded and logged; the panel's emission help parses again. The daemon starts on a host with its machine paths, release URLs and controller binaries from the environment, and the host cases for the routes, the tokens, the setup, the logs, the extension host and the cooling engine run through it in CI. - forgefirm-app 021323d -> 0f59c54 (0.1.31+git -> 0.1.32+git): the python3-gfhardware head, host tests only; it moves with the BSP's python3-gfhardware pin. - kas lock: meta-openglow f012d8e -> b7626be, the head that pins the kernel module f924cd9, python3-gfhardware 0f59c54 and gfutilities 993f9e1. grblHAL-glowforge keeps b34636f here: its CI runs this repository's harnesses from master, and its head's mcode_test expects the 5 s host build, so the driver is pushed after this and pinned on its own. Proof: image 20261015101725 carried forgectrl c9e3347, the module f924cd9 and this tree's forgetest, and campaign c-20261015103714-0c91 passed all 53 catalog tests on it with none inherited. bitbake -c fetch of forgectrl, gfcloud, gfhome, python3-ffmachine, python3-gfhardware, python3-gfutilities and kernel-module-glowforge from these pins succeeds (14 tasks). The tree manifest from the pins holds forgectrl at c9e3347 (229 files); the coverage lint over it finds no uncovered path, no empty entry and no host-tested row problem (53 tests); check-ui-vendor finds the shared UI files identical at c9e3347; test_tree_manifest passes, and the gate self-check refuses the empty artifact (rc 1). |
||
|
|
7f2da68e34 |
Catalog: image.license-bundle and image.network-boot leave; the network boot's board checks fold into image.health
Each left after its host proofs passed, with the coverage lint clean at the pins, its only failure the known setup.check-lens entry on src/lensfind.*: - image.license-bundle: the pack half is test_image.py's PackLicensesTests on pack-licenses.sh (forgefirm |
||
|
|
0d63f8740e |
pack-licenses.sh: the image's license step refuses a manifest without the project's recipes; the layer's interfaces file starts no DHCPv6 client
The host proofs of image.license-bundle's pack half and of image.network-boot's interfaces check. No catalog test is removed here; suite/image.py is unchanged. pack-licenses.sh, next to forgefirm-image.bb, is the license step. pack_licenses calls it with the common-licenses directory and the output tar path (forgefirm-image-dev.bb requires the release recipe, so both images run the one call), and do_rootfs's file-checksums name it. Before it writes the tar or removes the tree, it exits non-zero with a message on stderr when the directory is missing, when the manifest is missing, or when the manifest lacks any of the newline-terminated lines "RECIPE NAME: forgectrl", "RECIPE NAME: grblhal-glowforge" and "RECIPE NAME: kernel-module-glowforge". On success it packs as before: tar --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner, gzip -9 -n, the member common-licenses/license.manifest, and the loose tree removed once the bundle holds the manifest. test_image.py's PackLicensesTests drives that script on a fixture: a missing directory, a missing manifest, and a manifest missing each one of the three lines are refused with a message on stderr and no tar, the tree left; a manifest naming all three packs, the tar holds common-licenses/license.manifest, and the tree is gone. NetworkBootTests reads the BSP layer's interfaces file from the meta-openglow checkout beside forgefirm's (forgetest-ci checks out its scarthgap branch there) and finds no line that starts a DHCPv6 client. A missing file fails the case. meta-forgefirm's content hash moved (forgefirm-image.bb and the new script are layer content, and the platform identity of forgefirm-image-manifest.bbclass leaves out only *.md, *-pin.inc and forgefirm-release.inc): the next image cannot inherit a campaign. Proof: python3 -B -m unittest discover -s tests -p test_image.py on Linux (Ubuntu 24.04, GNU tar 1.35, meta-openglow at f012d8e): 14 OK, the five new cases among them; pyflakes names no undefined name. |
||
|
|
558889659c |
setup_burns: the module docstring describes setup.focus alone
setup.floor left the catalog in |
||
|
|
4c6ca8d701 |
test_setup_suite: only registered tests named
The file named setup.mirror (gone at |
||
|
|
f9c298a347 |
Catalog: eleven tests leave for their host proofs
Each left after its host cases passed (forgectrl 1dc85f6) and with the coverage lint clean at the pins, its only failure the known setup.check-lens entry on src/lensfind.*: - forgectrl.tokens: host_routes.sh's scoped tokens (a route refused to a token not given it, neither token in the list or the store), with tokens_test and auth_scoped_test. - update.product-gate: host_update.sh's product gate on the upload, with fwproduct_test. - setup.first-run-page: tests/wizard_walk.py. - setup.check-sensors, setup.check-intakes, setup.check-cameras, setup.check-flow-verify, setup.check-envelope, setup.floor: host_setup.sh's checks and the floor burn, with wizcalc_test. envelope_test.py's cases are unchanged. setup.floor's emission stays with setup.focus and laser.emission-witness. - motion.job: jobpost_test, with jobrun_test and lease_test; jobpost.* is covered by exthost.motion-job, which stays. - exthost.motion-jog: host_routes.sh's credential case. Two HOST_TESTED rows, each a binary forgectrl's build.yml runs at the pin c274f19 that proves the path the lint named: src/fwproduct.* to fwproduct_test, src/grblport.* to grblport_test. exthost.motion-job now requires motion.jog-roundtrip in place of motion.job. bedsize.py held setup.check-envelope alone and goes; forgectrl.py loses _words_of and its hw import, motion.py the job runner's helpers, setup_burns.py FLOOR_KEY and FLOOR_KEYS. forgetest's own tests: test_setup_dark.py loses the sensors and intakes judges and the departed ids; test_setup_burns.py loses the floor's cases, and the guards burn() and preview_ok() hold for the focus burn (the program's tail and reach, the thickness in inches and the one refused, a prompt not answered, the witnesses) run on setup.focus. Stay: forgectrl.auth (the fuse view's button refusal on the real button); forgectrl.settings-bounds (the lid lamp: cam.c writes /sys/glowforge/pic/lid_led, not under GF_SYSFS_ROOT); forgectrl.panel-serves (src/ui.h, which no binary at the pin proves; Z inside the lens reach is setup.check-lens's); logs.routing; setup.account-login (the account's render and its home, image content); setup.ssh-until-reboot (src/paths.h); setup.machine-name (udhcpc -x hostname and no mDNS responder, image content); setup.first-run-flow (src/button.*, led.*, advisories.*, hooks.h and the four advisory documents); exthost.motion-job (jobpost.*, which no binary at the pin proves). Proof: the coverage lint, 55 tests; pyflakes; test_setup_burns, test_setup_dark, test_takeover_position and test_image (54 OK). Shared text moved the fingerprints of exthost.armed-freeze, exthost.platform, exthost.service, forgectrl.auth, forgectrl.panel-serves, forgectrl.settings-bounds, laser.armed-kill, laser.emission-witness, laser.pause-resume-lid-cancel, motion.cancel-abort, motion.deadman, motion.jog-roundtrip, motion.liveness-probe, motion.microstep-modes, motion.pacing, motion.release, motion.step-timing-under-load, setup.account-login, setup.check-airflow, setup.check-lens, setup.check-motion, setup.check-switches, setup.cloud-header-capture, setup.first-run-flow, setup.focus, setup.machine-name, setup.ssh-until-reboot and update.slots-and-signature; exthost.motion-job's own text moved with its requires. |
||
|
|
754c239e08 |
test_image: the hostname and the banner scripts, run over a scratch tree
MachineNameTests runs the layer's own forgefirm-hostname and forgefirm-banner in private mount, UTS and network namespaces: /run a fresh tmpfs, a scratch directory over /sys/class/net, /etc a read-only overlay with the scratch hostname and issue over the host's (so each script makes its copy under /run/forgefirm and writes through its own bind mount, as at boot). unshare -Urmun where user namespaces are open, sudo -n unshare -mun otherwise; skipped only where neither makes a namespace. - the name: wlan0's last four hex digits (forgefirm-12ab with eth0 beside it), eth0's with no wlan0, eth0's with a zero wlan0 MAC; the live name and /etc/hostname agree. - the banner: the image's text, then the heading and one URL per global address of wlan0 (a veth carrying 192.0.2.5, 2001:db8::5 and fe80::5): https://192.0.2.5/ and https://[2001:db8::5]/, the link-local address left out, no marker line, no .local name. Proof: test_image.py under Linux by both namespace routes (9 tests OK), and on Windows with the two skipped. Mutants of scratch copies of the scripts, each failing: eth0 read before wlan0, a zero MAC taken, the file written without the script's bind mount, a marker line in the banner, a .local URL, the link-local address kept. setup.machine-name stays: the DHCP client's -x hostname and no mDNS responder on the image are image content. Nothing under meta-forgefirm changed. |
||
|
|
b3a40b4aea |
Catalog: twelve tests leave for their host proofs; image.health reads the board temperatures
Each test below leaves for the host cases that now hold it, every one
run and passing before this commit:
- cooling.fans-quiet-after-motion, cooling.gate-off (its engine half),
cooling.floor-and-warm-up, cooling.tec-drive, cooling.fire-watch-tiers,
cooling.crash-watch-plumbing, cooling.critical-tier and
cooling.fan-gate-trips: forgectrl's cool_flow_test and cool_gate_test
(forgectrl 8e9cc0a). The jog and M8/M9 report stays in grblHAL's
cooling_test.c and cool_report_test.py; real heater climb, a lit tube,
tach spin-down and fans meeting their floors stay with
cooling.flow-verify, cooling.flow-under-load, cooling.fail-tier-stop,
cooling.aa-offset-calibrate and setup.check-airflow.
- motion.gate-waits-for-lid: super_test D2 (forgectrl 8e9cc0a). The
physical lid read stays with setup.check-switches.
- motion.lid-cancel-home: laser_lifecycle_test's hold-lid-cancel (forgefirm
|
||
|
|
107676b77d |
laser_lifecycle_test: the lid cancels a job held on the button
Rule 16, hold-lid-cancel: a travel job held by a press, then the lid opened. The controller reports "lid opened - job canceled", prints the reset banner, raises no alarm, and returns the head to the job start (MPos X within 0.05 mm of where the job began); a press once the lid is closed again resumes nothing (Idle, the position unchanged). This is the part of motion.lid-cancel-home no host test held: the lid during Run is test_lid_cancels_and_returns, and the kernel playing the return stays with laser.pause-resume-lid-cancel's live lid phase (check_kernel_returned). Proof: the whole harness passes on the null-sink build as grblHAL's CI builds it (-DGFMCODE_WAIT_S=5.0): every earlier case, then hold-lid-cancel, lid-hold and sigterm-mid-job. Acceptance cost: none. scripts/bench/ is outside every fingerprint. |
||
|
|
6594f57362 |
Catalog: motion.port-jog and motion.tray leave; two leftovers of the audit
Two catalog tests leave for the host proofs that carry them; the catalog goes from 80 to 78 (78 @test in forgetest/forgetest/suite/). - motion.port-jog. forgectrl's tests/host_routes.sh (7b2f629 on e69d967) runs the controller port's routes against the port's stand-in: a jog refused without the token, a jog that is no number 400 with nothing on the port, a numeric jog on the port as G91 G21, cancel, release and energize each one line, GET /motion/state passing the port's state on with state and sender, GET /motion/sender 200. The port beside a sender, its polls, the cancel, the client first and the bound are grblHAL's ctlport_test.py and forgectrl's grblport_test; motion.release keeps the physical currents. The helpers before it (_port_state, _port_idle, _words) stay: bedsize, laser and motion.job read them. - motion.tray. host_routes.sh holds POST /motion/tray (tray=in on the port, tray=up 400) and the panel page's switch (id="traybtn", trayToggle, /motion/tray); grblHAL's tray_test.py the mode, its offset, its persistence and its frame; forgectrl's tray_test the daemon's side. suite/tray.py held only it: the module and its import go. The coverage lint then named forgectrl src/tray.c and src/tray.h, and nothing else; grblhal-glowforge's src/glowforge_tray.* stays covered by another catalog test, so it gets no row. One HOST_TESTED row takes forgectrl's, to forgectrl:tests/tray_test.c on forgectrl:.github/workflows/build.yml, which the pin c274f19 runs (./build/tray_test). Two of the audit's leftovers, in the same change: - test_manifest.py's ImplementationHashTests sat after the __main__ guard, so running the file directly skipped it. It is above the guard now, byte for byte: python3 -m unittest test_manifest.py and python3 test_manifest.py (from tests/, where its helpers import resolves) each run its 7 cases, and 34 in all; before, the direct run ran none of them. - laser_lifecycle_test.py's test_lid_open_in_wait said an alarm is raised; the docstring now says what the test asserts: the lid-open cancel is reported and no alarm is raised. The assertions are unchanged and the harness was not run. The coverage lint on the tree manifest finds no uncovered path and no host-tested row problem across the 78 tests; its one finding is setup.check-lens's empty src/lensfind.* entry. pyflakes reports nothing on motion.py, manifest.py, test_manifest.py or laser_lifecycle_test.py. The full forgetest suite was not run. Fingerprints, measured against the parent: 17 tests move with motion.py's shared text: the 11 left in motion.py, and homing.manual, laser's four and setup.check-envelope, whose modules read it. The pending pin bumps from forgectrl c274f19 and forgeext 2d0391e already owe all 17; this deletion adds none to the owed list. The two leftovers move no fingerprint. releases/ is unchanged. |
||
|
|
40c478d56c |
Catalog: forgectrl.lease leaves
forgectrl.lease leaves for forgectrl's tests/host_lease.sh (0086e65), which passed as build.yml runs it: with the gate open and cnc/state idle, the switches check held at its first wait names wizard:switches, kind hardware, as the holder; POST /diag/flow-verify, /curve/record, /logs/export, /mode with the mode in force, /controller/start and /settings are each 409 in the holder's name, none for an idle machine or a closed gate; the abort frees the lease and the same settings write is 200; lease.changed carries the take before the free. lease_test and super_test case K hold the lease's own rules. The catalog goes from 81 to 80 (80 @test in forgetest/forgetest/suite/). _EventStream, _lease and _text, which only it read, go with it, and so does urllib.parse, which only _EventStream used; socket stays for lan_ip. No test requires the id, and no unit test names it. The coverage lint on the tree manifest then named forgectrl src/events.c and src/events.h, and nothing else beside setup.check-lens's empty src/lensfind.* entry. One HOST_TESTED row takes them, to forgectrl:tests/events_test.c on forgectrl:.github/workflows/build.yml, which the pin c274f19 runs (./build/events_test). The lint then finds no uncovered path and no host-tested row problem across the 80 tests; its one finding is setup.check-lens's. test_manifest.py (34) passes, and pyflakes reports nothing on forgectrl.py or manifest.py. The full forgetest suite was not run. Fingerprints, measured against the parent: 26 tests move with forgectrl.py's shared text: the 4 left in it (auth, panel-serves, settings-bounds, tokens), the 5 left in exthost.py and the 17 of the setup family, whose modules import lan_ip from it (exthost.py and setup.py, and through setup.py the rest). The pending pin bumps from forgectrl c274f19 and forgeext 2d0391e already owe 24 of them; exthost.platform has been owed since |
||
|
|
70e13bdd88 |
Catalog: setup.mirror and logs.tree-tail-export leave; the catalog's address in forgetest-ci
Two catalog tests leave for the host proofs that carry them; the catalog
goes from 83 to 81 (81 @test in forgetest/forgetest/suite/).
- setup.mirror, for forgectrl's tests/host_ext.sh (0086e65): with a
temporary account and two logins, a package's check that session A
starts is owned and A's, owned and not B's, and the token's with no
session; B's abort is 409 and the run goes on; B's takeover makes it
B's; B's abort ends it aborted. Its section header, "the lifecycle",
held only it. setup.check-sensors and setup.account-login stay.
- logs.tree-tail-export, for forgectrl's tests/host_guard.sh (0086e65):
GET /logs names forgectrl, the tail carries a line just written, an
unknown logger is 404, and the export carries README.txt, the
installer's directory (a probe without its MAC or IP address, and
install.log) and system/extensions.json, with neither the panel token
nor the camera key in any member. _LOG_COVERS, the _PROBE_ constants,
and the gzip, io, and tarfile imports, which only it read, go, and so
does logs.routing's requires on it. logs.routing stays.
forgectrl.lease stays. Its proof passed (forgectrl's tests/host_lease.sh,
0086e65), but its deletion left forgectrl src/events.c and src/events.h
uncovered, a path outside the two this chunk may row, so it does not
land.
The coverage lint then named forgectrl src/sanitize.c, src/sanitize.h,
src/extpkg.c and src/extpkg.h, and nothing else. Two HOST_TESTED rows
take them, to forgectrl:tests/sanitize_test.c and
forgectrl:tests/extpkg_test.c on forgectrl:.github/workflows/build.yml,
which the pin c274f19 runs (./build/sanitize_test, ./build/extpkg_test).
forgetest-ci, after the tree-manifest step: the key comparison moves
there unchanged, and a step beside it checks the catalog's address.
From forgectrl-pin.inc's SRCREV it reads src/extpkg.h out of the
manifest cache (git show) and takes EXTPKG_INDEX_URL; from the
checked-out catalog's tools/publish.py it takes the basename of the file
passed to gh release create; the define must be exactly
https://github.com/openglow-org/forgefirm-extensions-catalog/releases/latest/download/
and that basename. A missing pin, define, or basename fails. Nothing
requests the address or fetches the asset. The step's run text, taken
from the workflow, passes from the tree root (c274f19's address and
index-1.ffi) and on a /tmp copy of the tree; controls on that copy,
publish.py passing index-2.ffi and extpkg.h naming another repository:
each fails the comparison. The key step's text is unchanged and was not
run again.
test_manifest.py (34) passes. The coverage lint on the tree manifest
finds no uncovered path and no host-tested row problem across the 81
tests; its one finding is setup.check-lens's empty src/lensfind.*
entry. pyflakes reports nothing on setup.py, logs.py or manifest.py.
The full forgetest suite was not run.
Fingerprints, measured against the parent: 23 tests move with
setup.py's and logs.py's shared text: the 5 left in setup.py, the 12 in
the modules that read setup.py (setup_dark.py's 9, setup_burns.py's 2,
bedsize.py's setup.check-envelope), the 5 left in exthost.py, which
reads it too, and logs.routing. The pending pin bumps from forgectrl
c274f19 and forgeext 2d0391e already owe 21 of them, and
exthost.platform has been owed since
|
||
|
|
b90299acdc |
Catalog: the package routes, the catalog, a package's check and the camera key leave
Four catalog tests leave for the host proofs that carry them; the
catalog goes from 87 to 83 (83 @test in forgetest/forgetest/suite/).
Each proof passed; none skipped.
- exthost.package-routes, for forgectrl's tests/host_ext.sh (05848ac):
against a compiled stand-in that is a live host, GET /ext/status and
POST /ext/package are 403 without the token with the host not run,
the status passes the host's list through with host.running true and
the stand-in's pid, enable runs as the host's command, a bad action
and a bad id are 400 with nothing run, and a package that is not
installed is 409 in the host's words. extpkg_test holds the relay's
argv; the state file, the required-holds directory and removal are
forgeext's install_test.py and run_test.py; the page card is
check_ui_js.py. REQUIRED_HOLDS, which only it read, and the "the hold"
section header over it go.
- exthost.wizard, for host_ext.sh (05848ac): the check listed, run to its
prompt with no lease holder, answered yes, ended with its summary and
listed done; the setup record's wizards and the gate as they were; a
second run aborted; a check of a package that is not installed 404.
wizpkg_test holds each step to its form. suite/extwizard.py held only
it: the module and its import go.
- exthost.catalog: install_test.py and extpkg_test already assert its
behavioral claims, and the one fact the bench test did not prove, that
the image's extension key is the catalog's published key, is now a
forgetest-ci step: the catalog is checked out at main as a sibling and
meta-forgefirm/recipes-forgefirm/forgefirm-keys/files/ext/forgefirm-ext.pub
is cmp -s'd against its keys/forgefirm-ext.pub, either one missing a
failure. suite/extcat.py held only it: the module and its import go.
- camera.key-read, for forgectrl's tests/host_lan.sh (05848ac): with
panel_open_reads=0, GET /cam/status from the LAN client over HTTP and
HTTPS is 403 with no key or a wrong one and 200 with the key as ?key=
or the header, the key alone writes no setting, and a rotation stops
the old key and the new one reads. camera.py's json import, which only
it used, goes.
The coverage lint then named forgectrl src/wizpkg.c, src/wizpkg.h,
src/camkey.c and src/camkey.h, and nothing else. Two HOST_TESTED rows
take them, to forgectrl:tests/wizpkg_test.c and
forgectrl:tests/camkey_test.c on forgectrl:.github/workflows/build.yml,
which the pin c274f19 runs (./build/wizpkg_test, ./build/camkey_test).
src/extpkg.* stays covered (logs.tree-tail-export, logs.routing), and
forgeext/** by exthost.service.
The key step's run text, taken from the workflow, passes from the tree
root with both files present; controls on a /tmp copy, a published key
that differs and one that is missing: each fails. test_manifest.py (34)
passes. The coverage lint on the tree manifest finds no uncovered path
and no host-tested row problem across the 83 tests; its one finding is
setup.check-lens's empty src/lensfind.* entry. pyflakes reports nothing
on exthost.py, camera.py or manifest.py. The full forgetest suite was
not run.
Fingerprints, measured: 10 tests move, the 5 left in exthost.py
(armed-freeze, motion-jog, motion-job, platform, service) and the 5 left
in camera.py (frame-health, h264-stream, lid-privacy, sensor-profile,
snapshot). The pending pin bumps from forgectrl c274f19 and forgeext
2d0391e already owe 9 of them; exthost.platform is owed already since
|
||
|
|
30c0a05b86 |
Catalog: exthost.panel-install leaves
exthost.panel-install leaves for the host proofs that carry it; the catalog goes from 88 to 87. - The upload sink is forgectrl's tests/host_ext.sh (005b159): 403 without the token, 409 and not idle while cnc/state reads running with the host never run, the host's refusal at 400 in its words with the staging file gone, an archive the host takes staged byte for byte as tier unverified with consent button, the discard, an install with nothing staged at 409, and a key without the button at 409 with no key written. The consent each tier takes, the phrase, the grants' form, the button the install and a key are passed, and the key's name and the host's refusal of it are forgectrl's extpkg_test; the tiers themselves are forgeext's install_test.py. - Its section header, "installing through the panel", held only it, and EXT_STAGE and the contextlib import, which only it read, go with it. Its requires line goes with the function. Three stay. exthost.package-routes: extpkg_test has the status document, the holds, disable and enable, remove, a bad action and a bad id at 400 with nothing run, and the host's refusal at 409, but not the 403 without the login on GET /ext/status and POST /ext/package, which is main.c's guard and no case of extpkg_test's. exthost.wizard: a package's check takes no lease (wizdark.c), and the lease is held in memory, so there is no lease file to assert absent at the prompt. exthost.catalog: install_test.py and extpkg_test assert every listed claim, and what is left is the image's own key. The coverage lint names no uncovered path, so no HOST_TESTED row is added: forgectrl's src/auth.* stays covered by other catalog tests, and src/extpkg.* by the three that stay. test_setup_suite.py (31) and test_manifest.py (34) pass. The coverage lint on the tree manifest finds no uncovered path and no host-tested row problem across the 87 tests; its one finding is setup.check-lens's empty src/lensfind.* entry. pyflakes reports nothing on exthost.py. The full forgetest suite was not run. Fingerprints: exthost.py's shared text moves, so 8 tests move: the 6 left in exthost.py, and exthost.catalog and exthost.wizard in the modules that read it. The pending pin bumps from forgectrl c274f19 and forgeext 2d0391e already owe 7 of them; exthost.platform is the one this adds. releases/ and TEST-AUDIT.md are unchanged. |
||
|
|
cfa6557847 |
Catalog: the sender keep-out, the event stream and the page's call leave
Three catalog tests leave for the host proofs that carry them; the
catalog goes from 91 to 88. Each proof passed; none skipped.
- exthost.sender-keep-out, for forgeext's run_test.py (ebdbf59): a
package granted sender.keep_out posts /v0/sender {"out": true}, the
stand-in takes it only with the host's token, and the claim is kept
fresh beside the holds, raised in the host's words, until the package
is disabled. The machine's side is forgectrl's senderout_test.
suite/extsender.py held only it: the module and its import go.
- exthost.events, for forgeext's run_test.py (ebdbf59) and forgectrl's
host_lan.sh (b272b3a): the host holds one stream while a package wants
events and lets it go when none does, a poll at the head comes back
empty at its one-second deadline, a waiting poll is answered 0.10 s
after an event; and the host's slot is a loopback peer's alone, the
same header from a LAN address leaving the host's stream open.
forgeext's evfeed_test and forgectrl's events_test hold the two
daemons' own sides. Its section header, the operator's door, held
only it, and EVENTS_MAX_STREAMS, EVENTS_HOST_HEADER and _stream, which
only it read, go with it.
- exthost.page-call, for forgeext's sdk_test.py (ebdbf59): disabled, the
page package's socket name is gone and its call is refused in the
host's words; enabled, the same call answers 200. sdk_test.py already
held the calls, their form and refusals, and the frozen service;
forgectrl's extpkg_test holds the relay. suite/extcall.py held only
it: the module and its import go.
The coverage lint then named forgectrl src/senderout.c and
src/senderout.h, and nothing else; one HOST_TESTED row takes them, to
forgectrl:tests/senderout_test.c on forgectrl:.github/workflows/build.yml,
which the pin c274f19 runs. grblhal-glowforge's src/serial.* stays
covered by a catalog test and gets no row; holdkeep, evfeed, call and
sandbox stay under exthost.service's forgeext/**, and forgectrl's
src/events.* under forgectrl.lease.
test_setup_suite.py (31) and test_manifest.py (34) pass. The coverage
lint on the tree manifest finds no uncovered path and no host-tested row
problem across the 88 tests; its one finding is setup.check-lens's
empty src/lensfind.* entry. pyflakes reports nothing on exthost.py or
manifest.py. The full forgetest suite was not run.
Fingerprints: exthost.py's shared text moves, so 9 tests move: the 7
left in exthost.py, and exthost.catalog and exthost.wizard in the
modules that read it. The pending pin bump from c274f19 already owes 8
of them; exthost.platform is the one it adds, as before. releases/ and
TEST-AUDIT.md are unchanged.
|
||
|
|
b5f355cb61 |
Catalog: exthost.ui-delivery leaves
exthost.ui-delivery leaves for the host proofs that carry it; the catalog goes from 92 to 91. - forgeext ui, in forgeext's tests/install_test.py (23156a4): an installed package's page comes back byte for byte as the html string, with bytes its length; an id that is not installed and a package the operator disabled are each refused in words. - forgectrl's relay of the page and the settings, in tests/extpkg_test.c on forgectrl's build.yml: the page served, a disabled package's page 404, the settings read, a value over its bound 400. The install-time mismatch between the ui capability and a page stays with forgeext's own tests. It has no glob of its own, so no HOST_TESTED row. exthost.sender-keep-out stays: forgeext carries a package's claim on the Grbl sender to the machine with the token it reads only from /run/forgefirm/ext-host.token, so run_test.py does not prove it, and suite/extsender.py is unchanged. test_setup_suite.py passes (31 tests). The coverage lint on the tree manifest finds no uncovered path and no host-tested row problem across the 91 tests; its one finding is setup.check-lens's empty src/lensfind.* entry. pyflakes reports nothing new in exthost.py. The full forgetest suite was not run. Fingerprints: exthost.py's shared text moves, so 12 tests move: the 8 left in exthost.py, and exthost.page-call, exthost.catalog, exthost.wizard and exthost.sender-keep-out in the modules that read it. The pending pin bump from c274f19 already owes 11 of them; exthost.platform is the one it adds, as before. releases/ and TEST-AUDIT.md are unchanged. |
||
|
|
b447ae77f1 |
Catalog: the LAN redirect, the release check, and the update job's lease leave
Three catalog tests leave for the host proofs that carry them; the catalog goes from 95 to 92. - setup.https-only-writes, for forgectrl's tests/host_lan.sh (13273a0): from a LAN peer in a network namespace, reads served over HTTPS and HTTP, three writes over HTTP each a 302 to the same path on HTTPS that acts on nothing, the same write served from loopback, and the token writing over HTTPS with no account. - update.release-check and update.job-locks, together, for forgectrl's tests/host_update.sh (acc3dcf), against a release server of its own: GET /update/release's keys, POST /update/check with a v<semver> tag, a size above 0, notes and new; the dismissal refused for a version that is not a tag, taken for the tag, cleared by an empty one; and a probe download that holds the machine (a settings write and a G21 job each 409 in the job's name, update:download of kind system the holder), ends refused by the signature check leaving no file, and lets the same settings write through after. update.release-check was the last test in suite/update.py; _VERSION_RX and the re import, which only it used, go with it. update.job-locks was the only test in suite/updlock.py: the module and its import in suite/__init__.py go. setup.https-only-writes was the only test of setup.py's "HTTPS boundary" section. tests/test_setup_suite.py drops its id. update.product-gate stays: fwup is on neither this host's PATH nor CI's, so host_update.sh does not prove the five uploads, and no HOST_TESTED row is added for src/fwproduct.*. No other HOST_TESTED row either. test_setup_suite.py passes (31 tests). The coverage lint on the tree manifest finds no uncovered path and no host-tested row problem across the 92 tests; its one finding is setup.check-lens's empty src/lensfind.* entry. pyflakes reports only the unused local plain in update.slots-and-signature, which HEAD already has. The full forgetest suite was not run. Fingerprints: setup.py's and update.py's shared text move, so 33 tests move: the 6 left in setup.py, the 25 in the modules that read it, and update.slots-and-signature and update.product-gate. The pending pin bump from c274f19 already owes 31 of them; setup.machine-name and exthost.platform are the two it adds, as before. releases/ and TEST-AUDIT.md are unchanged. |
||
|
|
21046cd6ff |
Catalog: setup.cert-page leaves
setup.cert-page leaves for forgectrl's tests/host_guard.sh (4d2d109): GET /settings reports a 95-character tls_fingerprint, and over HTTP and over HTTPS, with no token, GET /cert is 200 with no redirect, that fingerprint and the host's name, and GET /cert.pem is a PEM certificate. The catalog test reached the board by its LAN address; the page is what it proved. The catalog goes from 96 to 95. It called no helper of its own. src/tls.* stays on the login, HTTPS, first-run and TLS-record tests, src/ui/wizard.html on image.license-bundle and setup.mirror, and src/ui/wizard.js on forgectrl.auth, exthost.wizard and setup.mirror. No unit test names it. No HOST_TESTED row: the pinned workflow does not run host_guard.sh. test_setup_suite.py passes (31 tests). The coverage lint on the tree manifest finds no uncovered path and no host-tested row problem across the 95 tests; its one finding is setup.check-lens's empty src/lensfind.* entry. pyflakes is clean on setup.py. The full forgetest suite was not run. Fingerprints: the deletion moves suite/setup.py's shared text, so 32 tests move: the 7 left in setup.py and the 25 in the modules that read it. The pending pin bump from c274f19 already owes 30 of them; setup.machine-name and exthost.platform are the two it adds, as before. releases/ is unchanged. |
||
|
|
098cfdf193 |
Catalog: setup.record-export leaves
setup.record-export leaves for forgectrl's tests/host_guard.sh (84779c8 on d21e200): the record as JSON with the token (schema 1, the sheet id GET /wiz reports, no password hash, a plain read not an attachment), the download an attachment of the record named with that id, the printable page text/html with no script, the sheet id, "setup record" and an <h3> per completed wizard, both routes 403 without a token or a login, and the sanitized export carrying system/setup.json with the same id and wizards, no $6$, and no panel token anywhere in the archive. The catalog goes from 97 to 96. One HOST_TESTED row: forgectrl src/recordhtml.*, the one path no other catalog test covers, to tests/recordhtml_test.c (no script, the sheet id, the step headings, the escaping), which forgectrl's build.yml runs at the pinned revision. src/setup.* and src/wiz.* stay on the setup family, src/logs.* on exthost.service, forgectrl.lease and the two log tests, and src/main.c, src/ui/index.html, src/ui/panel.js and src/ui/wizard.html on the tests that already name them. tests/host_guard.sh is not named: the pinned workflow does not run it. tests/test_setup_suite.py drops the id, the record-export hardware assertion, and the export lines of the lifecycle covers test (the covers set and its two assertions); the mirror's assertions stay. test_setup_suite.py passes (31 tests). The coverage lint on the tree manifest finds no uncovered path and no host-tested row problem across the 96 tests; its one finding is setup.check-lens's empty src/lensfind.* entry. pyflakes is clean on setup.py and test_setup_suite.py. The full forgetest suite was not run. Fingerprints: the deletion moves suite/setup.py's shared text, so 33 tests move: the 8 left in setup.py and the 25 in the modules that read it (setup_dark, setup_burns, bedsize, exthost, extcall, extcat, extsender, extwizard). The pending pin bump from c274f19 already owes 31 of them; setup.machine-name and exthost.platform are the two it adds, as before. releases/ is unchanged. |
||
|
|
1e28eb92cd |
Catalog: setup.factory-return leaves
setup.factory-return leaves for forgectrl's tests/host_guard.sh (d21e200): POST /restore/factory-return with the token and no confirm is 400 and names confirm, confirm=0 is 400, GET /update/status shows no job running, GET /slots lists the archives (run only where /usr/sbin/ffboot is not on the host), and GET /setup carries the confirmed call; confirm=1 is never sent. The catalog goes from 98 to 97. It was the only test in the "the operator" section of suite/setup.py, and called no helper of its own. tests/test_setup_suite.py drops its id and test_the_factory_return_never_runs_the_return. src/update.* stays covered by the update tests, src/ui/wizard.js by the wizard and mirror tests, and src/main.c by the tests that already name it. test_setup_suite.py passes (31 tests). The coverage lint on the tree manifest finds no uncovered path and no host-tested row problem across the 97 tests; its one finding is setup.check-lens's empty src/lensfind.* entry. pyflakes is clean on both files. The full forgetest suite was not run. No HOST_TESTED row: the pinned workflow does not run host_guard.sh. Fingerprints: the deletion changes suite/setup.py's shared text, so the 34 tests that read it move again: the 9 left in setup.py, setup_dark 9, setup_burns 2, bedsize 1, exthost 9, extcall, extcat, extsender and extwizard 1 each. The pending pin bump from c274f19 already owes 32 of them; setup.machine-name and exthost.platform are the two it adds, as before. releases/ is unchanged. |
||
|
|
33e8c6e43f |
Catalog: logs.level-settings leaves
logs.level-settings leaves for forgectrl's tests/logs_test.c (9317b54): the six levels taken and verbose and 7 refused, port 70000 refused, udp and tcp taken and tls refused, a server name with spaces refused, and a remote level changed since boot listed as configured against effective with pending_reboot raised, and lowered once they match. The settings route's refusal of a value its validator rejects, with the file left byte for byte, is tests/host_routes.sh's bounds case. The catalog goes from 99 to 98. It was the last test in suite/logs.py and called no helper of its own, so nothing else goes. logs.tree-tail-export and logs.routing stay: they are the only catalog covers of forgectrl src/fflog.*, and the pinned workflow runs neither fflog_e2e.sh nor the new log tests. setup.record-export keeps its requirement on logs.tree-tail-export. The coverage lint on the tree manifest finds no uncovered path and no host-tested row problem across the 98 tests; its one finding is setup.check-lens's empty src/lensfind.* entry, which the forgectrl pin bump clears. pyflakes is clean on logs.py. The full forgetest suite was not run. Fingerprints: the deletion changes suite/logs.py's shared text (the two separator lines), so logs.tree-tail-export and logs.routing move; no other module imports it. The pending forgectrl pin bump from c274f19 already owes both, so this commit adds none to it. releases/ is unchanged. |
||
|
|
71556b2480 |
Catalog: the setup gate, the advisories, the extensions' consent and cloud-off leave
Four catalog tests leave for the host proofs that carry them; the catalog goes from 103 to 99. - setup.gate-blocks-controllers, setup.advisories-rehash, setup.extensions-consent and setup.cloud-disabled-surface. forgectrl's tests/host_setup.sh (91d30f1) runs the four HTTP proofs against the built daemon with the controller up: each reason the gate holds on and the live fan-count close and reopen; the first-run advisories, their hashes, and an acceptance that takes the press back; the extensions' advisory, its five refusals, and the consent; and every cloud selection refused while cloud mode is off, POST /mode included. builtin_test.c holds the cloud sweep, sha256_test.c the digest, and settings_dir_test.c the search bit the consent adds to the data directory. - gate_is_open and _GATE_COVERS go with the gate test, ADVISORY_DOCS and the hashlib import with the rehash; nothing else calls them. The record builders, REQUIRED_STEPS, SAFETY_PHRASE, Restore and request stay. - The requires that named them: _FIRST_RUN_REQUIRES held only the two deleted ids and goes, so setup.first-run-flow requires nothing from it and setup.first-run-page requires setup.first-run-flow alone; exthost.service keeps exthost.platform. - Coverage: HOST_TESTED rows for forgectrl src/sha256.* (tests/sha256_test.c) and src/builtin.* (tests/builtin_test.c), both run by forgectrl's build.yml at the pinned revision. docs/advisories/** stays behavioral: setup.first-run-flow, which accepts them, covers safety-and-risk.md, licenses.md, privacy.md and cloud-service.md, and exthost.service, which turns extensions on over it, covers extensions.md. - tests/test_setup_suite.py drops the four ids from the id list and the takeover list, the gate test's precheck assertion (the method now names the intake prechecks only), and CloudDisabledSurfaceTests, with the imports only that class used; the docstring no longer describes it. test_setup_suite.py passes (32 tests). The coverage lint on the tree manifest finds no uncovered path and no host-tested row problem across the 99 tests; its one finding is setup.check-lens's empty src/lensfind.* entry, which the forgectrl pin bump clears. pyflakes is clean on the four files. The full forgetest suite was not run. Fingerprints: deleting the four functions changes suite/setup.py's shared text, so 35 of the 99 tests' implementation hashes move: every remaining test in setup.py (10), and the modules that import it at module level, setup_dark (9), setup_burns (2), bedsize (1), exthost (9), extcall (1), extsender (1), extwizard (1), and extcat (1, through its import of exthost). The next forgectrl pin bump already owes 33 of them; exthost.platform and setup.machine-name are the two it adds. releases/ and TEST-AUDIT.md are unchanged. |
||
|
|
7172bba92b |
Catalog: the report channel, the event stream and what-changed leave
Three catalog tests leave for the host proofs that now carry them; the catalog goes from 106 to 103. - cooling.report-channel. forgectrl's tests/host_routes.sh (e69d967) runs the channel against a live daemon: a run report with the controller's secret writes exhaust 65535; no secret, a wrong secret and a short secret are 403 and the duty file is unchanged; an accepted idle while the ring still reads running writes 32768; the secret dies with the controller. cool_report_test.py and super_test case L own the secret, and cooling.fans-quiet-after-motion owns the live M8/M9 return to the idle duty. The forged Host: localhost was the same secret refusal. - events.stream. events_test.c owns the hello, the replacement, and motors.released; host_routes.sh owns the live cap (three peers held, a fourth 503, GET /settings 200 beside them, closing one admitting the next). forgectrl.lease and exthost.events still cover src/events.*, and _EventStream stays for forgectrl.lease. - setup.what-changed. setup_test holds tray -> laser.focus and fan -> airflow under the image's table beside tube -> laser.floor; host_routes.sh holds /wiz/changed (403, 400) and that every step the menu names is one GET /wiz lists. src/setup.* and src/wiz.* stay covered by the other setup tests, and help.js, panel.js and index.html by camera.key-read. preserved() and flag_of() go with it; recommended() stays. tests/test_setup_suite.py drops setup.what-changed from the id list, the takeover list and the covers lookup, and its flag_of case keeps only the recommended() assertion. test_setup_suite.py passes (36 tests). The coverage lint on the tree manifest finds no uncovered path across the 103 tests; its one finding is setup.check-lens's empty src/lensfind.* entry, which the forgectrl pin bump clears. pyflakes on the four files reports only the unused t0 in cooling.py that HEAD already has. The full forgetest suite was not run. Fingerprints: the cuts change the shared text of suite/cooling.py, suite/forgectrl.py and suite/setup.py, so 56 of the 103 tests' implementation hashes move (cooling 12, setup 26, forgectrl 5, and exthost 13 through its import of suite/forgectrl.py). The next forgectrl pin bump already owes 54 of them; exthost.platform and setup.machine-name are the two it adds. releases/ is unchanged. |
||
|
|
8e1fe5090d |
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. |
||
|
|
520d62ac05 |
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
|
||
|
|
8536ae7076 |
laser_stream_test: the analog copies, the in-run levels and the curve run leave
The analog dose model runs on no machine; the density sessions beside these prove the same rules on the shipped model. - Sessions J and K (M5 at idle, the next job's first cut) run under density only. - Session C (churn, analog) goes; its pad budget (no runaway pad across cycle churn) moves onto session G, density-churn, which already held FIRE, termination and the gaps on the same job. - Session H (a level change inside a run costs no byte) goes: every density power byte is already held at full duty by check_density; the byte count was a cost, not an energy rule. - Rule 19's end-to-end curve run goes: laser_arm_test.c case P pins the curve's mapping. The rolloff sessions keep the curve in force. - The constants only those sessions read go (CURVE_DEFAULT, curve_density_for, CURVE_S, JOB_CURVE, JOB_LEVELS); the header's rules 11, 16 and 19 say what the harness now holds. Proof: laser_stream_test passes whole on a null-sink build of the grblHAL-glowforge working tree under Linux (23 sessions). pyflakes is clean. Acceptance: none. The harness runs in grblHAL-glowforge's CI. |
||
|
|
09cf9242a9 |
laser_lifecycle_test: the sessions the unit tests and the stream prove leave
Each case below repeated a rule another test in CI already proves, read assertion by assertion next to it in the 2026-10-13 test audit: - test_job_window: laser_arm_test.c case I (S changes, and now M5 then M3, inside the window never re-prompt; df7087b) and laser_stream_test session K (M2 closes the window, the next job re-arms). - test_hold_grace: test_resume_after_grace makes the same assertion. - test_verdict_blocks_arm, test_arm_waits_for_engine_ack, test_arm_proceeds_on_a_late_ack: laser_arm_test.c cases C, A2, A3 and cooling_test.c's arm-time gate. test_arm_waits_for_the_fans stays. - test_sender_change: laser_arm_test.c case H; the disarm on a client change is unconditional. - test_jog_does_not_extend_window: laser_arm_test.c case S10. - test_verdict_fail_tier_ends_job: laser_stream_test rule 25 and laser_arm_test.c case V2. - test_verdict_pause_resumes_without_press: laser_stream_test rule 24, which now counts one arm across the pause, and case V1. - test_sender_change_mid_job: merged into test_sender_change_holds_and_rearms, which takes its "held inside the move" position check. - test_sender_change_then_reset: the window is closed and the latch locked by the sender change itself (case H); a reset from Hold is not energy. The header's rules 1, 2, 5, 11, 12 and 14 keep their numbers and name the tests that now prove them. Proof: laser_lifecycle_test passes whole on a null-sink build of the grblHAL-glowforge working tree under Linux (17 sessions), and laser_stream_test with it. pyflakes is clean; no helper was left without a caller. Acceptance: none. The harness runs in grblHAL-glowforge's CI. |
||
|
|
280ceee849 |
laser_stream_test: rule 24's pause holds the window open: one arm
Rule 24 proved the pause tier holds lit to the stop, masks FIRE while held, never writes the latch and resumes lit on the clean verdict. What only laser_lifecycle_test's verdict-pause session held was that the window stayed open through it: the resume armed nothing new. Rule 24 now counts the arms, so that session can leave. - verdict-rehold: exactly one "laser armed" across the pause, the re-hold and the resume. Proof: laser_stream_test passes whole on a null-sink build of the grblHAL-glowforge working tree under Linux. pyflakes is clean. Acceptance: none. The harness runs in grblHAL-glowforge's CI. |
||
|
|
8149a44f93 |
laser_stream_test: session B's FIRE-gap check moves to its density twin
Session B ran JOB_M3_TERM under the analog model; session F runs the same job under density and checked its span, density and termination, but not check_fire_gaps (no stepless run carrying FIRE), which B alone held. F now runs it, and B goes: analog never runs on a machine, and the analog ladder keeps its own termination and gap checks. - check_fire_gaps on density-full, its worst gap in the PASS line. - Session B's run and checks go; the JOB_M3_TERM comment names F. Proof: the harness passes whole on a null-sink build of the grblHAL-glowforge working tree under Linux, density-full with a worst fire gap of 270 ticks. pyflakes is clean. Acceptance: none. The harness runs in grblHAL-glowforge's CI. |
||
|
|
c91f6daa9e |
laser_stream_test: rule 14 runs under the density model
Rule 14 (a level set while the stream is idle still cuts at that level) ran only under the analog model, which no machine runs: its check counted fire ticks per analog duty. Under the shipped density model the level is the FIRE density at full duty, and nothing proved a stale level there. - Session I runs JOB_IDLE_S under DENSITY_CONF_FLOORED and judges it with rule 18's check_density: every power byte at full duty, and each move's span at its own S's density (23/127, 46/127, 81/127), not the one before it. The stream still ends dark. - The header's rule 14 says the level is judged as the FIRE density. Proof: the harness passes whole on a null-sink build of the grblHAL-glowforge working tree under Linux (densities 0.1812, 0.3624, 0.638 against 0.1811, 0.3622, 0.6378). A control judging the same stream against the levels in reverse order fails. pyflakes is clean. Acceptance: none. The harness runs in grblHAL-glowforge's CI. |
||
|
|
f8dc21b309 |
forgetest: motion.button-hold-resume leaves the catalog
motion.button-hold-resume held on the machine that the button holds a travel job and the next press resumes it to its target. The host proves it now: laser_lifecycle_test rule 7 runs the same job (M5, G91, G1 X40 F300) through the toggle and lands it within 0.05 mm of +40 ( |
||
|
|
a0a4edb322 |
laser_lifecycle_test: a travel job takes the button toggle and lands on its target
Rule 7 proved the button's pause/resume toggle on an armed job only, and nothing checked that the move resumed from the hold reaches its target. The catalog's motion.button-hold-resume held both on the machine with a travel job; it can leave the catalog once the host does. - test_button_pause_resume, after its armed half, runs the catalog's job: M5, G91, G1 X40 F300 (8 s). It must reach Run without asking for the arm press; a press holds it part-way; a second press resumes it to Idle within 0.05 mm of +40. - Rule 7's line in the header and the case's docstring say so. Proof: the case passes on a null-sink build of the grblHAL-glowforge working tree under Linux (the armed half as before; the travel job held at 8.747 mm and resumed to 39.999 mm). A control, the same job held and soft-reset instead of resumed, ends at 8.747 mm, which the landing check fails. pyflakes is clean. Acceptance: none. The harness runs in grblHAL-glowforge's CI. |
||
|
|
67ac8f4064 |
forgetest: events.button-telemetry leaves the catalog
events.button-telemetry held on the machine that one physical press reaches the event stream as pressed then released through its 25 Hz switch poll, and the telemetry event's fields. The host proves the rules now: - the press between two 5 Hz gathers, idle snapshot, scripted switch word: forgectrl events_test test_short_press (d8d07fc); - the telemetry field string, and that a press the machine is waiting for (arming, armed, leased, a button wait) is no event: events_test test_button. The physical button reaching the switch word stays in the catalog: setup.check-switches. - evmore.py, the test's own module, goes with its import in suite/__init__.py; nothing else imported it. - Its covers (forgectrl src/events.*, main.c, status.*) stay covered: events.stream holds all three, and other catalog tests hold main.c and status.*. No HOST_TESTED row is needed. Proof: the coverage lint reports 0 uncovered paths and 0 host-tested row problems over 107 tests (its one failure is setup.check-lens's src/lensfind.* entry, which the forgectrl pin bump clears); the gate self-check refuses an empty artifact cleanly. The unit modules that load the whole catalog pass under Linux: test_artifact, test_campaign, test_cooling_order, test_fixture, test_manifest, test_operator. Acceptance: the catalog hash moves; no remaining test's fingerprint moves (the module goes whole). |
||
|
|
0350b943d7 |
forgetest: laser.py's unused motion imports, and the two helpers no test calls
laser.py imported wait_state, wait_state_text, wait_left_state and
drain_text from motion.py and used none of them. wait_state and
drain_text stay in motion.py, which uses them; wait_state_text and
wait_left_state had no caller anywhere, so they go.
Proof: pyflakes no longer reports laser.py's four unused imports and
adds no finding. The coverage lint reports 0 uncovered paths and 0
host-tested row problems over 108 tests (its one failure is
setup.check-lens's src/lensfind.* entry, which the forgectrl pin bump
clears); the gate self-check refuses an empty artifact cleanly; the
forgetest host suite passes under Linux (539 tests).
Acceptance: the shared text of laser.py and motion.py moves, and with
motion.py's the tests of every module that imports it (motion, laser,
tray, updlock, and bedsize through homeoff). Against the image on the
bench that adds no run:
|
||
|
|
35add7cad0 |
forgetest: laser.power-floor leaves the catalog; laser.py's dead names go
laser.power-floor read $$ off the controller and checked $35 is the floor, that it lands at PWMSAR 12 or above, and that $31 is 0. The host proves all of it now: laser_stream_test rule 18 reads $$ on a fresh controller and after the arm ( |
||
|
|
d7379d9fc0 |
laser_stream_test: rule 18 reads $31 and the floor off the controller
Rule 18 proved a typed $35 is overwritten at the arm and the ladder
renders through the floor key, but took the shipped settings on faith:
the harness mirrors $31 = 0 and the board's 10 % floor as constants. The
catalog's laser.power-floor read them off the machine; it can leave the
catalog once the host does.
- The floor-derived session reads $$ on the fresh controller (no floor
key: the shipped default) and again after the arm overwrote the typed
$35. Both reads must show $31 = 0 (the floor, not $31, sets the bottom
of the S range) and $35 = the board's floor, and that floor must land
at PWMSAR_FLOOR_MIN (12) or above.
- A set floor key is laser_arm_test.c case J's (12.5 honored, an
out-of-range key falling back to 10, 0 honored). forgectrl keeps no
default for laser_floor_density: an absent key is the controller's
own floor, so there is no second default to compare.
Proof: laser_stream_test passes whole on a null-sink build of the
grblHAL-glowforge working tree under Linux ("$31=0 and $35=10 (PWMSAR
12) fresh and after the arm"). A control session with a floor key of 5
reads back $35 = 5 both times through the same parse, which rule 18
would fail. pyflakes is clean.
Acceptance: none. The harness runs in grblHAL-glowforge's CI.
|
||
|
|
21f53e4e83 |
forgetest: laser.verdict-cut leaves the catalog; the stream rules hold its claim
laser.verdict-cut held, on the machine, that a stale cooling verdict
mid-cut holds the job dark under the open window with the latch
untouched, and that the clean verdict resumes it lit with no press.
Every part of that is a rule the host now proves:
- dark while held: laser_stream_test rules 24 and 27, check_hold_dark on
every verdict hold's stationary stretch, and check_fire_gaps (
|
||
|
|
ef15c55577 |
laser_stream_test: the verdict's holds ship no FIRE while held
Rules 24 (the pause tier, held and held again) and 27 (a stale verdict) asserted the deceleration into each hold ran lit to the stop, the stretch between the holds dark, and the cut lit after the clear, but not that the stream is dark inside a hold: the central claim of the catalog's laser.verdict-cut, which can leave the catalog once the host proves it. Rule 21 proved it only for a plain feed hold. - check_hold_dark: no FIRE tick inside a hold's stationary stretch, its edges aside (one step pulse straddles each edge: rule 21's margin, HOLD_EDGE_TICKS, 120 ticks at x8 scaled to the mode: 480 at the x32 default). - Rule 24 checks both of its holds; rule 27 checks its one hold. - check_fire_gaps runs on both sessions, as on rule 21's. - The header's rules 24 and 27 say so; their PASS lines report the held stretches. Proof: laser_stream_test passes whole on a null-sink build of the grblHAL-glowforge working tree under Linux: verdict-rehold dark while held for 22620 and 22622 ticks, verdict-stale for 22621, check_fire_gaps clean on both. A negative control on check_hold_dark: a dark 5000-tick hold passes, and one FIRE tick inside it fails. pyflakes is clean. Acceptance: none. The harness runs in grblHAL-glowforge's CI. |