Skip to content

fix(tests): fail when a chart runs no Helm unit test suites - #3632

Merged
Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
fix/helm-unit-tests-detect-missing-suites
Aug 12, 2026
Merged

Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
fix/helm-unit-tests-detect-missing-suites

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

helm unittest is fail-closed on a suite it cannot parse and on one declaring tests: [], and fail-open on suites that are not there at all: it prints Test Suites: 0 passed, 0 total and exits 0. hack/helm-unit-tests.sh judges each package by that exit code alone, so a chart whose suites were deleted, moved, or renamed past the tests/*_test.yaml glob reports success having asserted nothing.

That is measurable in both directions rather than argued. Move packages/system/velero/tests/velero_test.yaml aside and run the script as it exists on main today:

Running tests in packages/system/velero
helm unittest .

### Chart [ cozy-velero ] .


Charts:      1 passed, 1 total
Test Suites: 0 passed, 0 total
Tests:       0 passed, 0 total

The run ends with All Helm unit tests passed. and exit 0. The chart passed, and the count of things it checked was zero. With this PR the same tree exits 1 and names the package and the reason. Neither --strict nor an explicit non-matching -f glob changes this on its own; all three forms exit 0, checked against plugin 1.1.1.

So the script now reads the captured output and fails the package when the zero-suite line appears.

Matching that line names a cause and a remedy where it applies, but the set of ways to run nothing is not closed. A recipe that never invokes helm unittest, and a test: rule that make finds nothing to do for, print no marker of any kind and exit 0, so no additional match reaches them; enumerating absences leaves a fresh hole every time a new shape turns up. So the script also requires the line a real run always emits, a Test Suites: count of at least one. That turns the question round: every way of asserting nothing exits 0, and "what did this run assert" is the question that has an answer. All 76 packages the script visits emit that line today. The tradeoff is that a test target which legitimately runs no helm unittest would need renaming or an opt-out, which is the same bargain the zero-suite check already strikes.

The discovery gate has the same shape from the other side. make -C dir -n test succeeds against a file-backed target, so a path named test beside a package Makefile whose rule is not phony would make both the gate and the run exit 0 without the recipe ever firing. The check keys on what make reports rather than on the path existing, because a package that declares the target phony runs its recipe whatever sits next to it, and refusing that would be a false failure. The quoting around the target name differs between make 3.x and 4.x, so it accepts either opening quote; anchoring on the trailing quote alone would also match a sub-make reporting some other target whose name ends in test.

That second check is guarding a live gap rather than a hypothetical one. hack/package.mk line 2 reads .PHONY=help show diff apply delete update image, which is a variable assignment and not a target declaration, so it makes nothing phony. Of the packages the script visits that define a test rule, 18 declare it phony in their own Makefile; the rest are unprotected if a path of that name ever appears. No such path exists today. Fixing the shared include is #3353 and is not this PR.

One deliberate non-change, stated so it is not rediscovered as an oversight. Running the suite now captures output instead of streaming it, because a pipeline's exit status in POSIX sh reports the last command rather than make. Streaming reads better and loses the status, so it stays buffered.

The run is pinned to LC_ALL=C, since both checks match English wording and a localized make would disarm one of them silently, which is the failure mode the whole change exists to remove.

The guard is all-or-nothing, which is worth stating plainly. A chart that loses four of its five suites still reports Test Suites: 1 passed and passes; only total disappearance is caught. Tying the expected count to what each chart actually has would be a per-chart number to maintain, and a number maintained in one place while the suites move in another is the failure this repo has been paying for elsewhere, so the cheap check that catches the total loss is the one worth having.

One behaviour worth stating because the message does not: a suite in which every test carries skip: reports Test Suites: 0 passed, 1 skipped, 1 total and exits 0, and the positive-evidence check refuses it. That refusal is intended, since a wholly skipped suite asserted nothing, but the message talks about suites expected under tests/, which in that case are present and skipped on purpose. No package is in that state today. Partial skips are unaffected: 2 passed, 1 skipped, 3 total satisfies the check.

Three known gaps in what the change ships, none of them a wrong statement. The positive-evidence message explains its cause but stops short of naming a remedy, where the other two messages both end in an action; the remedy for the legitimate case, renaming the target or giving it an opt-out, currently lives only in the code comment. That same message enumerates two ways a run reports nothing, and the skipped-suite case above is a third, so it reads as a diagnosis where it is really a list of the common causes. And no document in the tree states the convention this tightens: docs/agents/overview.md still describes the script accurately as running over every package that defines a test target, but the contract is now that such a package must also report at least one passing suite. All three are improvements to make on the next touch of these files rather than reasons to hold the change.

One adjacent gap stays open, and it is worth naming rather than leaving someone to assume otherwise. This catches a chart whose suite files vanished; it does not catch a package that loses its test: rule along with them. Nothing ties the presence of tests/*_test.yaml to the presence of a test target, so a change that removes both is skipped in silence, and the script only complains when no package in the tree has a test target at all. The test named a package with no test target is skipped, not failed pins that permissiveness deliberately, because the many packages that legitimately define no test rule would otherwise turn every run red. Closing it needs an instrument keyed on the suite files rather than on the Makefile target, which is a separate change.

hack/helm-unit-tests.bats adds eight tests, picked up automatically by the hack/*.bats glob in make unit-tests. Three pin the new refusals, and each matches its own message so none can stay green on a refusal that came from another. Two pin what must not be refused: a phony target with a colliding path, and a sub-make reporting an unrelated target up to date. One pins that a chart which does run suites still passes, which is what stops the refusals being satisfied by rejecting everything. Two pin behaviour that was already there, namely that a failing package is still reported by name and that a package with no test rule is skipped rather than failed. That last one builds a tree carrying a second package that does run a suite: with only the target-less package present, the script takes an early exit above the failure summary and the assertion would hold whatever the script had done. The fixtures stub make output instead of invoking helm, so they need no plugin installed.

Relates to #3453.

Screenshots

Downstream repositories

Release note

fix(tests): `make helm-unit-tests` now fails a chart that runs no Helm unit test suites, instead of reporting success for a chart whose suite files were deleted, moved, or renamed out of the `tests/*_test.yaml` glob

Summary by CodeRabbit

  • Bug Fixes

    • Improved Helm test validation to detect skipped test recipes, zero discovered suites, and runs without evidence of a passing suite.
    • Helm test failures now provide clearer diagnostics, including affected directories and failed commands.
    • Test output is consistently captured and replayed for easier troubleshooting.
  • Tests

    • Added comprehensive coverage for successful, failed, skipped, empty, and invalid Helm test scenarios.
    • Expanded coverage across package test targets, including phony and unrelated sub-make behavior.
  • Documentation

    • Clarified requirements for package test targets and suite reporting.

@github-actions github-actions Bot added area/testing Issues or PRs related to testing (e2e, bats, unit tests) kind/bug Categorizes issue or PR as related to a bug size/L This PR changes 100-499 lines, ignoring generated files labels Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The Helm unit-test script captures and validates test output, rejects incomplete suite execution, and reports failed package directories. The Bats suite covers target handling, suite evidence, failures, successful runs, sweep coverage, and packages without test targets.

Changes

Helm test validation

Layer / File(s) Summary
Script execution validation
hack/helm-unit-tests.sh
The script captures fixed-locale output, cleans temporary files, and records failures for unsuccessful commands, skipped targets, zero suites, and missing passed-suite evidence.
Script behavior test coverage
hack/helm-unit-tests.bats
Bats fixtures and tests verify target handling, diagnostics, exit statuses, suite evidence, successful and failing packages, sweep coverage, and packages without test targets.
Helm test target guidance
docs/agents/overview.md
The documentation defines the required package test target behavior, the no-suite failure condition, and the exception for the testing sandbox flow.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related issues

Possibly related PRs

Suggested labels: area/ci

Suggested reviewers: kvaps

Sequence Diagram(s)

sequenceDiagram
  participant PackageDirectory
  participant HelmUnitTests as helm-unit-tests.sh
  participant Make
  participant Helm
  PackageDirectory->>HelmUnitTests: discover package test target
  HelmUnitTests->>Make: execute make test with captured output
  Make->>Helm: run Helm unit suites
  Helm-->>HelmUnitTests: return suite output
  HelmUnitTests-->>PackageDirectory: report package result and summary
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: failing when a chart runs no Helm unit test suites.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/helm-unit-tests-detect-missing-suites

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This tightens hack/helm-unit-tests.sh so a chart whose suite files were deleted/moved/renamed out of tests/*_test.yaml fails the sweep instead of silently reporting success (helm-unittest prints Test Suites: 0 passed, 0 total and exits 0 for "no suites found"). It adds two more checks in the same spirit: a shadowed test Makefile target (make -n test succeeding against a file-backed, non-phony target) and a general "no positive evidence of a passing suite" backstop. hack/helm-unit-tests.bats adds 8 fixture-based tests for the new logic.

Verdict: LGTM on the logic and tests. Marking with notes only because of mergeability (see below), not because of a code defect.

Verification performed (static + isolated dry run)

  • Ran the PR's head version of hack/helm-unit-tests.sh for real (real helm + helm-unittest v1.0.3, no stubs) against all 76 packages the script currently visits. Result: zero ERROR lines, zero Test Suites: 0 passed, 0 total occurrences, zero is up to date shadowing hits, script ends with All Helm unit tests passed. — confirms the PR's core claim that the stricter check introduces no false failures on the current tree.
  • Ran the new hack/helm-unit-tests.bats (8 tests) through the repo's own hack/cozytest.sh runner: all 8 pass.
  • Independently confirmed the PR-body claim that exactly 76 package directories define a discoverable test target (make -C dir -n test succeeds) — matches.
  • Independently confirmed the PR-body claim about hack/package.mk:2 (.PHONY=help show diff apply delete update image, a variable assignment, not a target declaration) — real, pre-existing, out-of-scope-for-this-PR issue as stated (tracked separately as #3353).
  • Read the two new grep -qE checks closely for shell correctness: rc=0; LC_ALL=C make -C "$dir" test > "$OUTPUT_FILE" 2>&1 || rc=$? correctly captures output and status under set -eu without the "pipeline masks make's exit code" trap the old code would have hit if streamed through a pipe. The backtick/quote character class for the make 3.x/4.x "up to date" message is correctly escaped inside the double-quoted string (verified it survives as a literal character class, not a command substitution).

Non-blocking notes

  1. Mergeability (CONFLICTING is real, not stale metadata): a 3-way git merge-tree of base/head/origin/main shows a genuine both-added conflict on hack/helm-unit-tests.bats — PR #3670 ("close the silent-green gaps in e2e selection and the helm unit sweep", merged) independently created a file at the same path to cover a different gap (the packages/tests loop omission for cozy-lib-tests). It also touched the same header comment and for package_dir in ... line in hack/helm-unit-tests.sh, though that part appears to 3-way-merge cleanly on its own. Please rebase onto current main and manually reconcile the two .bats test suites (not a mechanical/no-op rebase) before this can merge.
  2. CI "E2E Tests" is currently red on this PR, but the failing subtest is specifically chainsaw/kubernetes-latest with a Kamaji guest-control-plane connectivity signature (Unable to connect to the server: ... i/o timeout, no route to host) while kubernetes-previous passed in the same run — this matches the already known, currently-undiagnosed intermittent node-join flake, and is unrelated to this PR's diff (which touches only hack/helm-unit-tests.sh/.bats). "Unit & controller tests" (which exercises the new .bats suite) passes cleanly.
  3. The PR description already self-documents several deliberate, deferred gaps (the positive-evidence error message names a cause but not a remedy; docs/agents/overview.md isn't updated for the new contract; a package that loses both its suite files and its test: rule simultaneously is still missed). These are reasonable to defer rather than block on.
  4. No actionable items from the existing automated CodeRabbit pass, and no human reviews on record yet.

Closing

The change is well-reasoned, the risky "hard fail on zero suites" behavior was verified empirically (not just asserted) to be safe against the entire current package tree, and the accompanying tests are meaningful (each pinned to its own distinguishing message so they can't stay green for the wrong reason). Please rebase onto main to resolve the conflict with #3670 (reconciling both .bats suites), then this is ready to merge from my side.

@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the fix/helm-unit-tests-detect-missing-suites branch from abf620e to c3ea97a Compare August 11, 2026 15:52
@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the fix/helm-unit-tests-detect-missing-suites branch 3 times, most recently from 57ac0c3 to 385a963 Compare August 11, 2026 16:32
@github-actions github-actions Bot added size/XL This PR changes 500-999 lines, ignoring generated files and removed size/L This PR changes 100-499 lines, ignoring generated files labels Aug 11, 2026
@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the fix/helm-unit-tests-detect-missing-suites branch 2 times, most recently from b8a5b9b to 385a963 Compare August 12, 2026 15:44
helm-unittest is fail-closed on a suite it cannot parse and on one
declaring `tests: []`, and fail-open on suites that are not there
at all: it prints "Test Suites: 0 passed, 0 total" and exits 0.
Measured on plugin 1.1.1 against a throwaway chart, all three
forms.

hack/helm-unit-tests.sh judges each package by that exit code
alone, so a chart whose suites were deleted, moved, or renamed past
the tests/*_test.yaml glob reports success having asserted nothing.
Moving packages/system/velero/tests aside reproduces it: the script
prints "All Helm unit tests passed" and exits 0.

Check the captured output for the zero-suite line and fail the
package when it appears. Neither `--strict` nor an explicit
non-matching `-f` glob does this on its own; both exit 0.

Matching that line names the cause where it applies, but the set of
ways to run nothing is not closed: a recipe that never invokes
helm-unittest, and a `test:` rule make finds nothing to do for,
both print no marker at all, so no further match reaches them. So
also require the line a real run always emits, a `Test Suites:` count
of at least one. Every way of asserting nothing exits 0, and asking
what the run asserted is the question that has an answer. All 77
packages the script visits emit that line today. The tradeoff is
that a `test` target which legitimately runs no helm-unittest would
need renaming or an opt-out, which is the bargain the zero-suite
check already struck. That is a contract contributors have to know
about, so docs/agents/overview.md states it where it describes the
sweep.

Because the check matches helm-unittest's own summary wording, a
plugin upgrade that reworded it would fail every package at once
while blaming each package's Makefile. The error text says so, so
the reader of a tree-wide red has the real cause in front of them
rather than 77 copies of the wrong one.

The discovery gate has the same shape from the other side.
`make -C dir -n test` succeeds against a file-backed target, so a
path named `test` beside a package Makefile whose rule is not phony
would make both the gate and the run exit 0 without the recipe
firing. Of the packages the script visits that define a `test` rule,
18 declare it phony, and no such path exists today. Match what make
reports rather than looking for the path, because a package that
declares the target phony runs its recipe whatever sits next to it,
and refusing that would be a false failure. The
quoting around the target name differs between make 3.x and 4.x, so
the match accepts either opening quote; the trailing quote alone
would also match a sub-make reporting another target whose name
ends in "test".

Running the suite now captures output rather than streaming it,
because a pipeline's exit status in POSIX sh reports the last
command rather than make. The capture file is allocated once and
removed by the EXIT trap that already owns the failure list, and the
run is pinned to LC_ALL=C: all three checks match English wording,
one on make's and two on helm-unittest's, so a localized make would
disarm the first of them silently, which is the failure mode this
guard exists to remove. The locale reaches every package's recipe,
not only make.

Extend hack/helm-unit-tests.bats with eight tests. Three pin the new
refusals; two pin what they must not refuse, a phony target with a
colliding path and a sub-make reporting an unrelated target up to
date; one pins that a chart which does run suites still passes; two
pin behaviour that was already there, namely that a failing package
is still reported by name and a package with no `test` rule is
skipped rather than failed. That last one carries a second package
that does run a suite, because with only the target-less package
present the script takes its early exit above the failure summary
and the assertion holds whatever the script did. The fixtures stub
make output instead of invoking helm, so they need no plugin
installed.

Both tests that assert a package is named in the failure summary
match the summary's "  - " prefix rather than the bare path. The
script announces "Running tests in $dir" before it runs anything, so
the bare path is present in the output whatever the summary does;
matching it asserted nothing, and the summary could stop naming
packages entirely with the suite still green.

The failing-package fixture reports a passing suite count alongside
its non-zero exit, so that the exit status is the only thing that
can decide the outcome. A fixture that fails without printing the
marker is refused by the positive-evidence check first, which leaves
the exit-status branch unpinned. That shape occurs in the tree: a
`test` target that runs helm-unittest and then a second step, a
render-parity script or a vendored-version guard or go tests, emits
the marker and still fails.

The two tests already in that file cover a package reached under
packages/tests, and their fixture printed a marker and nothing else.
That is the shape the positive-evidence check refuses, so the
fixture now also emits the line a real run emits; without it both
would fail for a reason unrelated to what they assert.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the fix/helm-unit-tests-detect-missing-suites branch from 385a963 to 94c41d1 Compare August 12, 2026 15:45

@myasnikovdaniil myasnikovdaniil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve. Rebase landed and the add/add on hack/helm-unit-tests.bats is resolved the right way: branch is 0 commits behind main, and the merged file keeps both packages/tests tests from main, the sweep runs the suites under packages/tests and a suite failing under packages/tests fails the sweep. That was my only blocker.

Ran the sweep myself on this head with env -u REGISTRY bash hack/helm-unit-tests.sh: 77 packages visited, exit 0, all passed. So nothing in the catalog newly reddens and no allowlist is needed. docs/agents/overview.md is updated too.

One thing stays open and it does not block. Guarded direction has zero live instances in the tree, the mirror direction has three: packages/apps/mongodb (7 suites), packages/system/gpu-operator (1), packages/system/monitoring-agents (1). That is 9 suites and 99 assertions that never run because those packages have no test: target, and every one of them passes when invoked by hand. Three one-line Makefile additions light them up. Either a follow-up, or retitle so "a chart runs no Helm unit test suites" is not claimed for a case that stays green.

@lexfrei
Aleksei Sviridkin (lexfrei) merged commit 3ad4d45 into main Aug 12, 2026
15 of 17 checks passed
@lexfrei
Aleksei Sviridkin (lexfrei) deleted the fix/helm-unit-tests-detect-missing-suites branch August 12, 2026 20:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/testing Issues or PRs related to testing (e2e, bats, unit tests) kind/bug Categorizes issue or PR as related to a bug size/XL This PR changes 500-999 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants