Repository navigation
test(tests): make negated BATS assertions able to fail - #3790
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe PR documents a rule against negated BATS assertions, adds a repository-wide scanner, converts existing assertions to explicit failure branches, and strengthens SeaweedFS Helm template parity checks. ChangesNegated BATS assertion enforcement
SeaweedFS template parity validation
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant BatsRunner
participant NegatedAssertionAudit
participant BatsSuite
BatsRunner->>NegatedAssertionAudit: Run repository audit
NegatedAssertionAudit->>BatsSuite: Scan .bats statements
BatsSuite-->>NegatedAssertionAudit: Report statements beginning with !
NegatedAssertionAudit-->>BatsRunner: Fail with replacement guidance
Possibly related issues
Possibly related PRs
Suggested reviewers: Merge Risk: ⚪ Minimal · up to The PR makes previously ineffective negated BATS assertions fail and adds a guard against regressions; no actionable merge-blocking risk remains beyond normal checks and review. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/agents/e2e-testing.md`:
- Line 141: Qualify the negated-assertion guidance so it says ! cmd is
unreliable and may pass unexpectedly rather than claiming it cannot fail; retain
the prohibition, including the tail-position bats exception. Apply this wording
change in docs/agents/e2e-testing.md:141-141 and AGENTS.md:31-31.
In `@hack/e2e-apps/monitoring-oidc-customconfig.bats`:
- Around line 123-128: Update both absence-check blocks in
hack/e2e-apps/monitoring-oidc-customconfig.bats:123-128 and
hack/e2e-install-cozystack.bats:617-621 to run kubectl get with
--ignore-not-found -o name, explicitly fail when kubectl returns an error, and
fail when the command produces non-empty output. Preserve the existing failure
messages for detected KeycloakClient and KeycloakClientScope resources.
In `@hack/seaweedfs-guard-parity.bats`:
- Around line 53-70: Replace the raw opener/closer counting checks with the
stateful parsing in template_code. Make template_code return nonzero when inc
remains set at EOF, while preserving its existing output behavior for valid
templates, and add a fixture covering {{/* inside an active Helm comment.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 1de527b3-132f-43fa-9a26-9bafd83c3d7e
📒 Files selected for processing (16)
AGENTS.mddocs/agents/e2e-testing.mdhack/bats-no-negated-assert.batshack/build-matrix_test.batshack/capture-dataplane.batshack/capture-previous-logs.batshack/common-envs_test.batshack/cozyreport.batshack/e2e-apps/monitoring-oidc-customconfig.batshack/e2e-install-cozystack.batshack/migration-50-etcd-adopt.batshack/migration-seaweedfs-db-adopt.batshack/nightly-mirror_test.batshack/promote-retag_test.batshack/release-changelog-contract.batshack/seaweedfs-guard-parity.bats
cd55ebf to
3c84002
Compare
22b8dd9 to
9cf097e
Compare
e450a5c to
e54ccf9
Compare
IvanHunters
left a comment
There was a problem hiding this comment.
LGTM.
Empirically confirmed the crux under real bats: the old ! echo|grep -q FORBIDDEN passes vacuously (exit 0) when the token is present, while the new if grep; then false; fi fails under both runners. Completeness is proven: an independent grep over hack/**/*.bats finds zero remaining negated asserts outside the guard file, and the new lexical guard (9/9) blocks regressions. Ran six converted suites, no not-ok, so no new always-failing asserts.
- [MINOR] hack/monitoring-oidc-customconfig.bats — the one conversion under hack/e2e-apps/ is not run by
make unit-testsand needs a live cluster; pattern is identical to the rest, verified statically.
b48f598
e54ccf9 to
b48f598
Compare
b48f598 to
e909d30
Compare
e909d30 to
5e45a8a
Compare
1a51af2 to
9a30fd3
Compare
The parity suite forbids four mutable discriminators -- claim
timestamps, ready-replica counts and the two birth-order flags -- by
grepping each chart's whole file. Both charts carry a template comment
block arguing why those signals were rejected, so the grep matched the
argument and would have reported a chart for documenting the rule it
follows. It never did report: the checks were spelled `! grep`, a form
POSIX exempts from errexit, so they could not fail whatever they found.
Strip the {{/* ... */}} spans before matching and let the loop fail
through `false`, naming the signal it matched.
The span and not the line. Dropping every line that opens a comment is
shorter and silently loses a discriminator that shares its line with
one -- `{{- $x := .creationTimestamp }} {{/* why */}}` leaves with the
comment and the checks find nothing. The two spellings agree on
today's charts, so a fixture pins the difference rather than trusting
it.
Have the strip say when it ended inside a comment, and ask before
reading what it returned. Every `{{/*` is read as an opener, including
one inside a quoted string -- these charts carry long `fail (printf
...)` messages -- and a lone opener swallows the rest of the file,
leaving the checks reading nothing. A canary token cannot cover that:
it only proves whatever precedes IT survived, and the block most
likely to swallow is the last one in the file.
It says so on stdout rather than through an exit status, because
hack/cozytest.sh rewrites every `}` at column 0 into `return 0; }` --
the strip's own closing brace included -- so a status it returned
would never reach the caller.
Counting delimiters cannot stand in for the state the strip already
carries. Go template comments do not nest, so the first `*/}}` closes
the block and `{{/* the opener is spelled {{/* */}}` is a comment Helm
renders, while its two openers against one closer read as an imbalance
that is not one.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
An assertion written `! cmd` cannot fail a test. POSIX exempts a command whose return value is inverted with `!` from errexit, so the status is dropped where it is produced. hack/cozytest.sh then closes each test function with `return 0`, and the bats binary's ERR trap carries the same exemption -- it reports only a negated assertion standing last in the body, where the inverted status happens to be what the function returns. Every absence these suites assert was written that way: a command that must not reach the log, an image that must not be copied, a flag that must not be passed, an object that must not exist. On that half of each suite's contract the tests held for any input. Rewrite each as `if cmd; then echo "FAIL: ..."; false; fi`, the form already used where the trap had been noticed one file at a time, and give every one a message naming what must not have happened. The message states the property, not the pattern: where the forbidden string is a substring of a required value -- an https endpoint the test requires in its http form -- naming the string would describe the opposite of what is asserted. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
9a30fd3 to
3453817
Compare
A negated assertion is the natural way to spell "this must not
appear", and an absence is what nothing else in a test covers, so the
errexit exemption lands where it is least likely to be noticed and
invisible in a green run. Switching runners does not lift it: the ERR
trap the bats binary reports through carries the same exemption, so
neither runner sees a negated assertion that has any statement below
it.
Add a suite that scans every hack/**/*.bats, subdirectories included,
and fails on a statement whose first token is `!` -- naming the file,
the line the statement opens on, and the form to use instead.
Statement boundaries are the POSIX separators, each measured against
the runner before being admitted: `;` `&` `&&` `||` `{` `(`, the `)`
closing a case pattern, and the words `then` `do` `else`. A keyword may
legally be preceded only by whitespace or by `;` -- every other
separator in front of one is a shell syntax error, checked with `sh -n`
rather than recalled -- so the scan admits exactly those two and that
part of the class is closed rather than patched a spelling at a time.
Both spellings are vacuous under the runner, so missing either is a
silent miss, the one direction this scan must not err in. For the same
reason the scan takes a `!` with no space before its subshell,
`!(cmd)`, which is vacuous on dash and bash alike, and reads through a
`time` or `time -p` prefix, which is vacuous under bats and under
cozytest where /bin/sh is bash. A scan that wants `!` and then
whitespace as the first token misses both. On a dash /bin/sh `time` is
not a keyword and `time ! cmd` fails, so there it is an over-count.
Five over-counts remain, all erring loud, each named in the header:
they are enumerated rather than removed, because a removed error
leaves no trace while a named one tells the next reader what they are
looking at.
Inverted conditions are untouched because errexit is not what decides
a condition. `[ ! -f x ]` is untouched for a different reason and
stays a normal failing assertion: the `!` there is an operator of
`test`, not negation of a command, so nothing exempts its status.
Each claim the suite makes is pinned against removing the thing it
claims, and the separator class is measured branch by branch rather than
only at its newest entries. The recursion is exercised through the audit
function on a fixture in a subdirectory rather than through a second
copy of its find expression, because a copy passes while the function
loses its recursion -- and an audit that returns empty reads as a clean
tree. The condition-strip carries fixtures that put the negation after
an `&&` inside the condition, where only the strip can save it. The
continuation fold is pinned by a fixture whose negation sits on the
continued line, so folded and unfolded name different lines. Both
keyword spellings, the `time` prefix and `!(cmd)` have fixtures of
their own. The subshell over-count carries a fixture for both of its
halves -- that the scan reports the shape, and that the shape really
does fail.
Both halves of the premise are pinned, not just the half about the
banned form. A test asserts the runner still passes a bare negated
assertion; a second asserts the prescribed `if cmd; then ...; false; fi`
does fail, under both runners, and fails AT the form rather than
somewhere before it. Every converted absence rests on that second
half, and its failure mode is asymmetric: were `false` in a `then`
body to stop propagating, the first test would still pass while every
conversion went silently green.
The rule joins the E2E conventions doc as its own section, and the
inline list in AGENTS.md that an agent may read instead of the doc --
it belongs beside the EXIT-trap ban for the same reason, both leaving
a green suite that checks nothing.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
Six suites explain why they avoid `! cmd` by saying errexit is suppressed for a "negated pipeline". The exemption is not about pipelines: a bare `! true`, a negated `[ ... ]`, one inside a `for` body or a brace group, one in a case branch, and one following `;`, `&`, `&&` or `||` are all exempt too. Anyone reading the narrower wording could conclude the other shapes are safe. One of the six carries the sentence twice, and the second copy is split across two physical lines -- `` `!` `` ending one and `pipeline)` opening the next -- so a line-oriented search for the phrase does not see it. Prose wraps; a grep for it has to fold the lines first. Comment text only; every one of these files already uses the working form the comment recommends. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
The assertion that the no-delete guardrail can be bypassed decides on `kubectl get`'s exit status alone. That status is 1 for a NotFound and 1 for a connection, auth or discovery failure, so an api-server the test cannot reach reads as proof the configmap is gone. `--ignore-not-found` collapses the NotFound alone into success with empty output, which leaves the status free to mean a failed read. Read the two separately: a non-zero status fails the test naming the read that did not answer, non-empty output fails it naming the object that must not be there. Capture stdout only. kubectl writes deprecation and throttling notices to stderr on a call that succeeded, and folding those into the value under test would report an object that is not there. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
3453817 to
0a3546e
Compare
IvanHunters
left a comment
There was a problem hiding this comment.
LGTM.
Test-tooling and docs only (hack/, AGENTS.md, docs/agents/e2e-testing.md), no product code moves.
I ran it rather than read it. Checked against head 0a3546e under bats 1.13.0, and the guard suite is green under cozytest too:
-
The premise holds on the runner itself. cozytest.sh injects
set -eper test andreturn 0before every column-0}, so! cmdis vacuous (the test "a negated assertion still passes under this runner whatever it finds" is green), whileif cmd; then echo FAIL; false; fifails under both cozytest and bats (the test "the form this ban prescribes does fail, under both runners" is green). A barefalseaborts under errexit from any position, not just the last line, so the prescribed form still fails when more assertions follow it. -
The guard reddens on regression. Put a
!-first assertion back into hack/promote-retag_test.bats and "no suite states an absence in a form that cannot fail" flips tonot ok. -
Completeness: no inert
!-first assertion is left in hack/**/*.bats outside the guard file. The surviving|| ! grepoccurrences are inverted conditions insideif ...; then, which errexit does not govern, and they are correctly left alone. -
seaweedfs-guard-parity: the comment strip earns its keep. A forbidden signal inside a
{{/* ... */}}comment stays green (documenting the rule is not classifying on it), while the same signal in executable template code reddens the discriminator check.
One thing for later, out of scope here: the guard enforces the ! ban, but an if cmd; then echo FAIL; fi that drops the false is just as inert and carries no !, so nothing flags it. Every conversion in this PR keeps the false, so this is a future footgun worth a line in the doc rather than a change request.
| # cannot reach would read as proof the delete worked. `--ignore-not-found` | ||
| # collapses the NotFound alone into success with empty output, leaving the | ||
| # status free to mean a failed read. | ||
| if ! still_there=$(kubectl get configmap cozystack-version -n cozy-system --ignore-not-found -o name); then |
There was a problem hiding this comment.
[NIT] this conversion also fixes a NotFound-vs-read-failure ambiguity
Not a mechanical ! cmd conversion: using if ! still_there=$(kubectl get ... --ignore-not-found -o name) plus [ -n "$still_there" ] separates a real NotFound from an unreachable api-server. Before, a read that failed for any reason would have read as proof the delete worked. Good catch.
There was a problem hiding this comment.
Yes, that was the point of the if ! form. The old one read any failed get as a successful delete.
…ion (#4738) ## What this PR does Migration 50 scales the legacy etcd-operator to 0, adopts every legacy `etcd.aenix.io` cluster with `etcd-migrate`, and then scales the operator back to 1. At that point the Deployment still runs the legacy operator, and after a successful adoption it has nothing left to manage. Bringing it back undoes part of the adoption. Observed on a production cluster upgraded from 1.4.3 to 1.6.4. The API audit log and the Helm history of a tenant's `etcd` release show this sequence, in UTC on the same day. - 13:21:12 migration 50 scales `cozy-etcd-operator/etcd-operator-controller-manager` to 0. - 13:22:59 `etcd-migrate` updates `etcd-headless`, pointing its owner references at the adopted EtcdMembers, and orphan-deletes the legacy EtcdCluster and StatefulSet. - 13:23:04 migration 50 scales the operator back to 1. It is still the legacy operator, the v1alpha2 one replaced it only at 13:40. - 13:23:42 Flux upgrades the tenant's `etcd` release with the old chart (Helm revision 87), because the new chart artifact appeared only at 13:24:40. The old chart re-creates the legacy EtcdCluster at 13:23:43. - 13:23:43 to 13:23:45 the legacy operator reconciles it, updates `etcd-headless` twice and re-creates the legacy StatefulSet `etcd`. - 13:36:49 Flux upgrades the release to the new chart (revision 88), which prunes the legacy EtcdCluster. - 13:36:50 the garbage collector deletes `etcd-headless` and the re-created StatefulSet. The same happened in all five tenants that run an etcd. The pods survived because EtcdMembers own them, but they keep `spec.subdomain: etcd-headless`, so their `etcd-<i>.etcd-headless.<ns>.svc` names stopped resolving. From then on every member logged `no such host` for its peers, the v1alpha2 EtcdCluster had no status conditions, and the `etcd-defrag` CronJob failed every hour with `context deadline exceeded`. The transitional `etcd-headless` Service the etcd chart ships for this window (#3270) does not cover it. Helm patched the existing Service 202 ms before the garbage collector removed it, and did not create it again because it was already part of the release. With this change the operator stays at 0 after a successful adoption. The etcd-operator HelmRelease replaces the Deployment with the v1alpha2 operator later in the same upgrade, and no later migration waits for the operator. A failed or interrupted adoption still restores it to 1 through the existing EXIT trap, so a failed run does not leave the tenant etcds without an operator. If that HelmRelease cannot reconcile, the adopted clusters wait for it with or without this change, because the legacy operator does not reconcile v1alpha2 EtcdClusters. Clusters already upgraded with this bug can be repaired without restarting anything. Creating the Service from the tenant's own `etcd` release is enough. ``` helm -n <ns> get manifest etcd | awk 'BEGIN{RS="\n---\n"} /\nkind: Service\n/ && /\n name: etcd-headless\n/' | kubectl -n <ns> create -f - ``` On the cluster above the v1alpha2 EtcdCluster reported `Available` with reason `QuorumHealthy` within 11 seconds of the Service being created, in every tenant, and the members stopped logging `no such host`. ### Validation - `hack/cozytest.sh hack/migration-50-etcd-adopt.bats` passes all 29 tests. A new test asserts that a successful adoption scales the operator to 0 and never back to 1. Against the current script it fails with `FAIL: the legacy operator was scaled back up after a successful adoption`. - The existing test for a failed adoption still passes, so the EXIT trap still restores the operator to 1. - The new test checks the absence with `if grep ...; then echo "FAIL: ..."; false; fi`, the form the other suites use. A bare `! grep` assertion cannot fail a test under `hack/cozytest.sh`, so it could not have caught this. The other negated assertions in this file had the same weakness, and #3790 has rewritten them on main. With all of them made able to fail in a scratch copy, the suite still passes against this change. ### Screenshots Not a UI change. ### Downstream repositories Walked the trigger map in `docs/agents/contributing.md` against the diff. It changes a migration script and its bats test. No package, values, schema, CRD, make target or `hack/` layout changes, so no entry matches. - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: - [ ] [cozystack/community](https://github.com/cozystack/community) - follow-up: ### Release note ```release-note fix(platform): keep the legacy etcd-operator stopped after migration 50 adopts the legacy etcd clusters. Scaling it back up let it reconcile the legacy EtcdCluster that Flux re-created from a not yet upgraded etcd chart, re-create the legacy StatefulSet and take the legacy etcd-headless Service away from the adopted members, which the garbage collector then deleted. The adopted members lost their DNS names, the EtcdCluster never reported a status and etcd defrag failed every hour. Clusters already upgraded can recreate the Service from the etcd release manifest. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Migration Updates** * After successful etcd adoption, the legacy operator remains scaled to zero replicas for replacement by the etcd operator; it is no longer scaled back up before the migration is stamped. * If adoption fails or exits before completion, the legacy operator is restored to one replica. * The dry-run message now describes the zero-replica outcome of successful adoption. * Successful adoption continues to record the migration version stamp. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
What this PR does
A hundred and twelve assertions across twelve BATS suites begin with
!. A hundred and seven of them pass no matter what they find. POSIX exempts a command whose return value is inverted with!fromset -e, so the failing status is dropped where it is produced, andhack/cozytest.shcloses each test function withreturn 0, so it never resurfaces as the test's own status either. The other five are followed by|| { ... exit 1; }, which reads the inverted status and does work; they are rewritten too, because telling the two apart needs the tail of every statement parsed, and a guard that guesses wrong there is green on a real one.Switching to the
batsbinary does not fix the hundred and seven. Its ERR trap carries the identical exemption:bash -c 'set -eE; trap "echo TRAP" ERR; ! true; echo REACHED'printsREACHED, runs no trap and exits zero. The only negated assertionbatsreports is one standing last in the test body, where the inverted status happens to be what the function returns. Two of the hundred and seven stood last; the rest did not.What was lost is specific rather than diffuse.
!is how you spell "this must not appear", and an absence is what no other assertion in a test covers. The affected suites assert that a migration ran nothing destructive, that no image was copied, that a flag was not passed, that a backup cleanup never flushed the master, that an object does not exist. On that half of their contract they held for any input.Each site is now written as
if cmd; then echo "FAIL: ..."; false; fi, the form already used in the files where this had been noticed one at a time, and each carries a message naming what must not have happened.hack/bats-no-negated-assert.batsscans everyhack/**/*.batsand refuses the old form, so a new one cannot be added silently. Inverted conditions (if !,while !,until !) are untouched, becauseset -eis not what decides a condition.[ ! -f x ]is untouched for a different reason and remains an ordinary failing assertion: the!there is an operator oftest, not negation of a command, so nothing exempts its status.The guard pins its own premise instead of describing it, on both sides. One test asserts the runner still passes a bare negated assertion; a second, that neither
set -enor the ERR trap sees an inverted status; a third, that the prescribedif cmd; then ...; false; fidoes fail, under both runners, and at the form rather than somewhere before it, since a run that died early would satisfy a bare status check and pin nothing.Every rewrite here rests on the third test. If
falseinside athenbody ever stopped propagating, the first test would still pass, because a negated assertion would still be vacuous, while every converted absence went silently green. A ban that prescribes a replacement has to test the replacement too.Every claim the guard makes is pinned by removing the thing it claims. The recursion is exercised through the audit function against a fixture in a subdirectory, not through a second copy of its
findexpression: a copy keeps passing while the function loses its recursion, and an audit that returns empty reads as a clean tree. Recursion matters becauseBATS_UNIT_FILESis a non-recursivewildcard, somake unit-testsnever runs a suite placed in a subdirectory.hack/e2e-apps/held such suites until they moved to Chainsaw and is gone now, which is why the test pins against a fixture and not a real directory.The rule's surface was checked in the other direction too. The shape appears in no
hack/**/*.shscript as an assertion: the one hit,hack/testdata/install-wait/call-shapes.sh, is fixture text thathack/run-kubernetes-install-wait_test.batsparses, not a command anything runs. It appears in no Chainsawscript:step either, so.batsis the whole surface rather than the part that was convenient to guard.The scan is lexical, so its header names the statement boundaries it owns rather than implying it understands shell:
;&&&||{(, the)closing a case pattern, and the wordsthendoelse, each measured against the runner before being added. A keyword may legally be preceded only by whitespace or by;, since every other separator in front ofthen,doorelseis a shell syntax error, checked withsh -nrather than recalled. The scan therefore admits exactly those two, and that part of the class is closed rather than patched one spelling at a time. All three keywords are vacuous after either spelling, so missing one is a silent miss, the single direction this scan must not err in, given that its whole design trades noise for safety. For the same reason it takes!(cmd)with no space before the subshell, which is vacuous on dash and bash alike, and reads through atimeortime -pprefix, which is vacuous underbatsand under cozytest wherever/bin/shis bash.Five over-counts remain and are deliberate, each named in the header:
( ! cmd )does propagate its inverted status and is reported anyway; so would a!opening a statement after a redirection&;x=$(true || ! false)is reported although the||reads the inverted status; a condition continued by a trailing&&instead of a backslash is reported, because the strip only recognises a condition whose end it can see; andtime ! cmdis reported although on a dash/bin/sh, which has notimekeyword, it runs an externaltimethat fails. Each costs one rewritten line, where the opposite error costs a silent pass. They are enumerated rather than engineered away on purpose, because a removed error leaves no trace while a listed one tells the next reader what they are looking at. The scan also errs quiet in at least two places, named here rather than in the header. A file cut off midway through a line continuation loses its last buffered statement, but that input is not a runnable test, sincesh -nrejects it withunexpected end of file. Andtime -- ! cmd, which bash accepts, is vacuous underbatsand not peeled by the scan; nothing in the tree uses it, and I stopped adding spellings there rather than chase every prefix bash allows. The test naming the shapes claims only what its fixture holds. A lexical scan over shell cannot promise it has enumerated the language, and a name that promised it would be false whether or not a gap existed today.The branch name is narrower than the subject and I am leaving it as it is rather than churn it. This is not a defect in
hack/cozytest.shthat moving to thebatsbinary would fix. It is a POSIX rule both runners inherit, and moving tobatswould close two of the hundred and seven sites.One suite went red for real once its assertions could fail.
seaweedfs-guard-parityforbids four mutable discriminators by grepping each chart, and both charts carry a template comment block arguing why those signals were rejected, so the grep matched the argument and would have reported a chart for documenting the rule it follows. The charts are correct; the assertion was not. It now strips{{/* ... */}}before matching. Two details there are load-bearing and neither is obvious. The strip removes the comment span, not the line that opens it: dropping the line is shorter and silently loses a discriminator sharing a line with a comment, which is a chart classifying on a forbidden signal and a test reporting that it does not. And the strip reports when the file ended inside a comment, and the checks refuse to read its output when it did, because every{{/*is read as an opener, so one inside a quoted string (these charts carry longfail (printf ...)messages) would swallow the rest of the file and leave the checks reading nothing.One commit widens six comments that explained this trap as applying to a "negated pipeline". It is not about pipelines, and the narrower wording invites the conclusion that the other shapes are safe. Comment text only; each of those files already uses the working form.
The last commit makes the one absence check in
hack/e2e-install-cozystack.batsthat already used theifform tell a missing object from a failed read. It asserted that the version configmap was gone onkubectl get's exit status alone, and that status is 1 both for a NotFound and for an api-server the test cannot reach.Verification
Every unit suite this branch touches was run through
hack/cozytest.shon the current base and is green.migration-seaweedfs-db-adoptpasses 15 of 15 on macOS only when its temp directory sits under$HOME. Its container cannot write into a bind mount under/var/folders, and/usr/bin/mktemp -dignoresTMPDIR, so the run needs GNUmktempfirst onPATHas well. Measured on Linux before the rebase, it passed 15 of 15 as is.The six sites that reached main after this branch was first written, in
redis-backup-strategyandproxmox-e2e-fixture_test, were each shown to be able to fail: with the condition inverted, the suite goes red at that assertion and prints its ownFAIL:message. The nine sites inmigration-seaweedfs-db-adoptcarry a two-sided check as well: with the pattern swapped for one the same test proves is present, the rewritten form fails the suite and the original!form passes it.The guard itself is green under cozytest with bash and with dash as
/bin/sh, under thebatsbinary, and with mawk, gawk and BSD awk. Each branch of its separator class and of thetimeand!(cmd)handling was removed in turn, and each removal turns the shapes test red.e2e-install-cozystack.batsis an e2e suite and runs only in the E2E workflow, so its one changed absence check is not exercised locally.Screenshots
Not a UI change.
Downstream repositories
Walked the trigger map against the diff file by file. The change touches eighteen existing BATS files, adds one, and adds a convention to
docs/agents/e2e-testing.md. It does not touchhack/e2e-prepare-cluster.bats, which is the node contractansible-cozystackandtalmrestate; it moves and renames nothing underhack/and changes no make target's behaviour, which is whatccpgates on; and it touches no package, schema, enum, default, CRD, label, annotation or metric.Release note
Closes #3565