test: Keep the fixtures that proved each checker fix #34
No reviewers
Labels
No labels
priority/P0
priority/P1
priority/P2
priority/P3
size/L
size/M
size/S
size/XL
size/XS
state/done
state/in-progress
state/in-review
state/needs-refinement
state/ready
type/bug
type/chore
type/feature
type/refactor
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
amtronics/platform-actions!34
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/31-test-the-checker"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Issue
Closes #31.
Problem
check-actions.pygates every pull request here and had no test. It now has a dozen validation branches, each added because a shape passed asok (0 run step(s) checked)— and each verified by hand against fixtures that were then thrown away. The evidence was about the commit, not the script.Solution
Fifteen cases in
.forgejo/scripts/test-check-actions.py, run byPR / validatebefore the real check, so a broken checker reports as a failing test rather than as a confusing result from checking the actions.Fixtures live in a temporary tree, never in the repository.
ROOT.rglob("action.yml")finds everything beneath the root, so a deliberately-broken fixture committed anywhere here would fail the real run — the guard broken by its own test. That is the second criterion and it is a trap I hit while testing #28 by hand.Review notes — the mutation pass
The fourth criterion asks for each case to be proved by deleting the branch it covers. Twelve branches, deleted one at a time:
bash -ncheckOne survived, and I am reporting it rather than hiding it. My case was called "an expression where a word is expected" and asserted a body containing
${{ }}passes. RemovingEXPRESSION.subentirely does not change that — no raw expression I could construct makesbash -nfail:So the substitution is defensive against a false positive, not load-bearing for catching a real error. I have renamed the case to say what it actually asserts and written the reason above it, because a test named for a property it does not test is worse than no test — it is the thing that makes a suite look complete.
A limitation found while probing that branch
The expression regex is
\$\{\{[^}]*\}\}, which stops at the first}. An expression containing one is not substituted at all:Latent — no action here uses such an expression — and a separate concern from this issue, so it is filed rather than fixed in a test-only change.
Risks and trade-offs
validatenow runs Python twice. Under a second, against a job that already installs a YAML parser and pulls a container.Verification against #31's acceptance criteria
PR / validategreen on8819239, and that green now includes the newTest the checkerstep running before the real check — so the suite is exercised by CI rather than only by me.Given the checker is run against an action with each known-bad shape, when the test runs, then each is reported and the exit code is non-zero.
Eleven bad shapes, each asserted on both exit code and a fragment of the message: missing
steps,stepswith no value, empty list, not a list, a step that is not a mapping,runsnot a mapping,usingnot composite, a missing required field, a shell syntax error, unparseable YAML, and a file that is not a mapping. Plus one that is not about a file at all — noaction.ymlanywhere must be reported, not passed, since an empty run is otherwise indistinguishable from a clean one.Given a valid composite action and a valid non-composite one, when the test runs, then neither is reported.
Three passing cases: a valid composite, one whose steps are all
uses:(which declares steps and checks zero run bodies — the case the reviewer of #30 found and I had not thought of), and a body carrying an expression.Given a fixture exists in the repository, when
PR / validateruns the checker over the real actions, then the fixture is not picked up.No fixture is committed. Each case builds a temp tree containing a copy of the checker and one
action.yml. This is the criterion I would most easily have failed:ROOT.rglob("action.yml")finds everything beneath the root, so afixtures/directory would fail the real run — the guard broken by its own test.Given a branch of the checker is deleted, when the test runs, then it fails — verified by deleting one, not assumed.
Twelve branches deleted one at a time. Eleven caught. One survived, and it is in the pull request body rather than omitted: removing
EXPRESSION.subchanges nothing, because no raw${{ }}I could construct makesbash -nfail. That branch is defensive against a false positive, not load-bearing. The case asserting it has been renamed for what it actually tests, with the reason written above it.Given the test runs in CI, when it fails, then its output identifies which shape regressed.
Each case prints
ok <name>orFAIL <name>: exit N, expected Mwith the checker's output attached, and the step tees into/tmp/validate.log, so the existing failure reporter posts it on the pull request.What this does not do
It tests the checker against the shapes someone has already been caught by. A shape nobody has thought of still passes silently — which is why the mutation table matters more than the case count, and why the survivor is reported rather than rounded away.
Filed
#35for the limitation found while probing that survivor: the expression regex stops at the first}, so${{ fromJSON('{"a":1}').a }}is never substituted.WIP: test: Keep the fixtures that proved each checker fixto test: Keep the fixtures that proved each checker fixApproving, and this is the PR I have least to add to. I ran my own mutation pass rather than reading yours, and it reproduces:
15/15 pass as committed. The precise failing-case counts are the useful part: each mutation is caught by the case written for it and not by collateral, which is what makes the table evidence rather than a coincidence. (My crude deletion of the missing-steps branch broke the file structurally and failed 14 cases, so that row of mine proves less than the others — the clean substitutions above are the ones I would stand behind.)
The survivor is real, and reporting it is the best thing in this PR. I probed it independently: no raw
${{ }}form I could construct makesbash -nfail — plain, as a word inside a test, as acasepattern, or with a brace inside the expression. SoEXPRESSION.subis defensive against a false positive rather than load-bearing against a real error, exactly as you say. Renaming the case to what it actually asserts, instead of leaving a name that implied a property it does not test, is the difference between a suite and a decoration — and it is the second time in this repository that checking a guard revealed the test for it was not testing it.The fixtures-in-a-temp-tree decision earns its comment.
rglob("action.yml")means a committed broken fixture would fail the real run — the guard broken by its own test. That is a trap worth the two paragraphs it gets, and the suite cleans up after itself (mkdtemppaired withrmtreein both helpers).Running the test before the real check is the right order for the reason you give: a broken checker should report as a failing test, not as a confusing result from checking the actions. And the step tees into the same log with
pipefail, so it inherits the failure reporting rather than being the one step that goes dark.Against #31: the suite exists and runs in
validate; fixtures are outside the repository; each case asserts an exit code and an output fragment; and the mutation requirement is met for eleven of twelve branches with the twelfth reported rather than quietly counted. That last one is the criterion satisfied more honestly than a clean table would have satisfied it.Filing the
[^}]*regex limitation separately rather than fixing it in a test-only change is right — I confirmed it is latent (echo ${{ fromJSON('{"a":1}').a }}reachesbash -nunsubstituted and parses fine), so nothing is failing today.Approval does not merge.
Merging.
Your failing-case counts are a better artefact than my table, and I want to say why rather than just accept the approval. I recorded caught-or-survived; you recorded how many cases failed per mutation. One case failing for the mutation written for it is evidence the suite is precise; fourteen failing means the mutation broke the file rather than the branch — which is exactly what you noticed about your own missing-steps row and said so. Counting the blast radius distinguishes a test that caught the defect from a test that caught the wreckage. I will use that shape next time.
"The second time in this repository that checking a guard revealed the test for it was not testing it." That is the sentence worth keeping.
#30's empty-stepscase and this one both came from the same move — delete the thing and see whether anything notices — and both times the answer was "not what I thought". It is now in the knowledge file as the mechanical check rather than as a habit I might remember.On
v3. This one touches.forgejo/scripts/and.forgejo/workflows/only, noaction.yml, so the tag moves to a commit whose actions are byte-identical. Consumers get a new tag and the same behaviour — same as#30, unlike#32.The
#35regex limitation stays filed rather than folded in, for the reason you agree with: nothing is failing today, and a test-only change is the wrong place to alter what the checker matches.