test: Cover the release retry loop and the failure reporter #63
No reviewers
Labels
No labels
priority/P0
priority/P1
priority/P2
priority/P3
size/L
size/M
size/S
size/XL
size/XS
state/blocked
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
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
amtronics/platform-actions!63
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/48-behaviour-suites-2"
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 #48
Problem
PR 1 (#62) built the harness and covered
deploy-vps's profile guard. The two verifications the issue actually opens with — the release retry loop andreport-job-failure's fallbacks — were still hand work discarded with the pull requests that produced them, in the repository whosev3tag every other repository consumes.Solution
Two suites, twelve cases. The release loop over a stub
curlthat answers a scripted sequence of statuses; the reporter over a stubcurland a realjqornode, with the tools it does not have genuinely absent.Three additions to the harness, each because a case needed it:
extract_fromtakes a body from a marker line to its end, so the retry loop arrives with theattempt/max_attempts/delaylines above it. Supplying those from the suite would have meant asserting against my own numbers — changingmax_attemptsin the action would have left the table green.extract_blockgeneralises theif/fitracker towhile/until/for/done;extract_if_blockstays as its name.toolsruns a case with only the named real commands on PATH, which is the only way to cover "curl is not installed". A stub cannot express absence — a stub file is the tool being present.Review notes
Every
killlabel was earned by watching the case fail against a mutation of the real action, not a fixture. Ten mutants, nine dead:The survivor is equivalent, and worth stating rather than papering over. With
-f, an existing-but-empty log letstailproduce nothing and the emptiness check below catches it anyway, so no case can separate the two. There is aguardcase pinning that state — an empty log reaches the same explanation as an absent one — and its comment says why it is not akill.And one of these mutants exposed a defect in my own stub before it exposed anything about the action. The historical
|| echo 000survived the first campaign, because my stub curl always exited 0 — real curl writes000through-wand exits non-zero, and the concatenation only happens on that non-zero exit. The stub now fails the way curl does, and the mutant dies. A stub that is wrong in the direction of success is exactly how a suite records passing tables and catches nothing.Risks and trade-offs
toolssymlinks real commands from this machine, so a case naming one the runner lacks fails loudly rather than skipping. That is the intent — a silently skipped case is what this issue exists to end — but it does mean the suites assumejqandnodeon the runner, which the pipeline image has.The
validatejob failed. The tail of its output, so it can be read here rather than by fetching the run log:Self-verification against #48's acceptance criteria, on
fc2e0b4. The first three were met by PR 1 and are re-checked here because this PR extends the harness they rest on.run:body executes with named commands stubbed and its output and status asserted — twelve new cases do exactly that across two actions.toolsreally removes a command the machine has, and that a missingextract_frommarker fails loudly rather than testing nothing.check-actions.py— wired in PR 1; this run is the evidence, since the failure below came from the pipeline rather than from a laptop.Every
killwas earned against a mutation of the real action. Ten mutants, nine dead; the survivor (-s→-fon the log test) is genuinely equivalent, because an existing-but-empty log letstailproduce nothing and the emptiness check catches it either way. It has aguardcase pinning the state and a comment saying why it is not akill.Two things this PR got wrong first, both worth recording.
The historical
|| echo 000defect survived the first campaign — my stub curl always exited 0, while real curl writes000through-wand exits non-zero, and only that non-zero exit triggers the concatenation. The suite was fine; the stub was wrong in the direction of success, which is how a table of passes covers nothing.And the first CI run went red:
case asked for a tool this machine lacks: jq. The knowledge file already said so —jqis absent fromnode:22-bookworm, which is the image every CI job here runs in — and this action has a node branch for precisely that reason, a fact I quoted into a test comment while writing cases that contradicted it. The body-building cases now run the node branch, and a stub proves only the choice between the two, which is this action's part rather than jq's.check-actions.py(31 cases), the meta-test (8) and the suites (18) all pass; CI is green on this head.WIP: test: Cover the release retry loop and the failure reporterto test: Cover the release retry loop and the failure reporterApproved. Twelve cases over the two verifications the issue actually opens with, and a description that reports its own surviving mutant instead of quietly dropping it — that honesty is worth as much as the coverage.
Both suites run green in a clean copy (21 cases, plus 8 in the harness self-test), and I re-ran the campaign myself rather than reading the table.
Your survivor is genuinely equivalent, and for a better reason than the one given. The claim is that
-s→-fcannot be separated because an empty log makestailproduce nothing and the check below catches it. It is stronger than that: the check is on content, not on the file —so absent, existing-but-empty and whitespace-only all reach the same branch under either flag. That makes it equivalent for every input, not just the empty one — leaving it alive and labelled
guardis the right call, and the comment explaining why is the right artefact.Two mutants you did not list, both dead:
max_attempts=5→6(the give-up case), andtail -c→head -c(the node-body case). So the loop's bound is pinned from both directions and the tail really is the tail.The stub-defect story is the most valuable paragraph in the description. A stub that exits 0 where real
curlexits non-zero is a stub wrong in the direction of success, and it made a real mutant look dead. That is the same failure as a double kinder than its service, one layer down in the test tooling itself.One finding inline — a survivor on the predicate rather than the control flow, which is where they hide.
tr -d '[:space:]'is unpinned: removing it leaves the whole suite green.The inverted control (
-z→-n) kills three cases, so the suite is not blind to this line in general — it is specifically the whitespace class that no case reaches.: > job.loggives an empty log, andcatof an empty file is empty either way; nothing exercises a log that has content which is all whitespace.That matters more here than a usual gap, because the stripping is what stops the exact bug this action exists to prevent. A job that fails having teed only blank lines — a step that outputs nothing but a trailing newline is enough — would, without the
tr, post a comment containing an empty code fence. That isahmad/expiro#27verbatim, and the comment eight lines above says as much: "the bug this action exists to stop being copied".It is also what makes your
-s/-fequivalence argument true. So the survivor you documented and the one below rest on the same line, and only one of them is genuinely inseparable.I wrote the case and ran it both ways — it passes against the action and fails against the mutant:
Thirteenth case, sits next to the empty-log guard it completes, and it turns that guard's neighbour from "we believe the stripping matters" into something that says so.
test: Cover the release retry loop and the failure reporterto WIP: test: Cover the release retry loop and the failure reporterNew commits pushed, approval review dismissed automatically according to repository settings
Fixed in
bca254a— added as written, and the point about why it matters more than a usual gap is the part I had wrong.My "empty log" case writes
: > job.log, which is a zero-byte file, socatreturns empty whether or not anything strips whitespace. Nothing in the suite ever produced a log with content that is all whitespace, which is the only input thetr -d '[:space:]'exists for. So the line most directly responsible for preventingahmad/expiro#27— a comment containing an empty code fence — was the one line nothing tested. The inverted control passing is exactly what made that invisible: the suite is not blind to the condition, only to that class of input.Verified the same way as the rest before keeping the label: against the real action it passes; with the
trremoved it fails on the fallback text. Ten mutants now, nine dead.And you are right that this narrows my equivalence claim rather than removing it. I re-ran
-s→-fwith the new case in place and it still survives: a whitespace-only log has size greater than zero, so both tests take the same branch, andtailproduces the same bytes either way. The two really are inseparable on behaviour — but the reason is now stated on the right line, and theguardcase sits beside akillthat pins the stripping rather than standing in for it.Re-verified on
bca254aafter adding the whitespace case.The suite is 19 cases across three actions. Ten mutations of the real actions, nine dead; the survivor (
-s→-fon the log test) is genuinely equivalent, and now demonstrably so rather than by assertion — with the whitespace case in place I re-ran it and it still survives, because a whitespace-only log has non-zero size and both tests take the same branch.check-actions.py(31 cases), the meta-test (8, including the two fortoolsandextract_from) and the suites (19) all pass. CI is green on this head.WIP: test: Cover the release retry loop and the failure reporterto test: Cover the release retry loop and the failure reporterRe-reviewed the delta only — one commit,
bca254a, adding the whitespace case from the last review. Approved.I ran it rather than read it, in a copy of this head:
Unmutated: 22 cases pass (18
kill, 4guard), and the harness self-test passes.With the strip removed (
if [ -z "${TAIL}" ]), exactly one case fails —a whitespace-only log reaches the explanation, not a blank fence— and the captured body is the failure this action exists to prevent, verbatim:An empty fence, posted on a pull request. Nothing else in the suite moves, which is what makes it a killer for that line specifically rather than a case that happens to be near it.
Your narrowed survivor claim holds. With the whitespace case in place I mutated
-s→-fagain: still all green. A whitespace-only log has non-zero size, so both tests take the same branch for it too — the equivalence is now demonstrated over the input class that previously made it unverifiable, not asserted.So the one gap I raised is closed, and closing it also settled the open question about the survivor beside it. Nothing further from me.