Files
cozystack/hack/helmrelease-readiness.bats
Myasnikov Daniil cc942fed68 test(tests): run unit suites with Bats and retain POSIX coverage
The shell translator rewrites test bodies and loses Bats reporting.
The unit lane needs complete TAP results and JUnit reports without
changing how live-cluster suites run.

Sourced production helpers still need a dash pass, and unit test bodies
need nounset without exporting shell options into subprocesses.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 11:04:00 +03:00

221 lines
7.7 KiB
Bash

#!/usr/bin/env bats
# Unit coverage for the single post-install HelmRelease readiness gate.
load test_helper
helmrelease_snapshot() {
count="$1"
not_ready="${2:-0}"
stalled="${3:-0}"
stale="${4:-0}"
jq -nc --argjson count "$count" --argjson notReady "$not_ready" --argjson stalled "$stalled" --argjson stale "$stale" '
{items: [range(1; $count + 1) as $i |
{metadata: {namespace: "ns", name: ("hr-" + ($i | tostring)), generation: 2},
status: {observedGeneration: (if $i == $stale then 1 else 2 end), conditions:
([{type: "Ready", status: (if $i == $notReady then "False" else "True" end), reason: "Progressing", message: "working"}]
+ (if $i == $stalled then [{type: "Stalled", status: "True", message: "terminal failure"}] else [] end))}}
]}
'
}
@test "stale Ready stays closed until the current generation is observed" {
. hack/e2e-wait-helmreleases.sh
clock=$(mktemp)
printf '0\n' > "$clock"
date() { sed -n '1p' "$clock"; }
sleep() {
now=$(sed -n '1p' "$clock")
printf '%s\n' "$(( now + $1 ))" > "$clock"
}
kubectl() {
now=$(sed -n '1p' "$clock")
if [ "$now" -lt 4 ]; then
helmrelease_snapshot 11 0 0 3
else
helmrelease_snapshot 11
fi
}
cozy_wait_all_helmreleases_ready 30 11 4 2 >/dev/null
[ "$(sed -n '1p' "$clock")" -ge 8 ] || {
echo "the gate accepted Ready=True from an unobserved generation" >&2
exit 1
}
}
@test "a late HelmRelease resets stability and is included before success" {
. hack/e2e-wait-helmreleases.sh
clock=$(mktemp)
calls=$(mktemp)
printf '0\n' > "$clock"
date() { sed -n '1p' "$clock"; }
sleep() {
now=$(sed -n '1p' "$clock")
printf '%s\n' "$(( now + $1 ))" > "$clock"
}
kubectl() {
printf '%s\n' "$*" >> "$calls"
now=$(sed -n '1p' "$clock")
if [ "$now" -lt 2 ]; then
helmrelease_snapshot 11
elif [ "$now" -lt 4 ]; then
helmrelease_snapshot 12 12
else
helmrelease_snapshot 12
fi
}
cozy_wait_all_helmreleases_ready 30 11 5 2 >/dev/null
[ "$(sed -n '1p' "$clock")" -ge 10 ] || {
echo "the gate returned before the late release became Ready and stabilized" >&2
exit 1
}
[ "$(grep -c -- '-o json' "$calls")" -ge 6 ]
}
@test "Stalled HelmRelease fails immediately without sleeping to timeout" {
. hack/e2e-wait-helmreleases.sh
calls=$(mktemp)
date() { printf '100\n'; }
sleep() { echo "sleep must not run after Stalled=True" >&2; return 1; }
kubectl() {
printf '%s\n' "$*" >> "$calls"
helmrelease_snapshot 11 3 3
}
if cozy_wait_all_helmreleases_ready 900 11 5 2 >/dev/null 2>&1; then
echo "a terminal Stalled HelmRelease passed readiness" >&2
exit 1
fi
[ "$(wc -l < "$calls")" -eq 1 ] || {
echo "the Stalled path kept polling instead of failing immediately" >&2
exit 1
}
}
@test "a transient list error keeps the gate closed" {
. hack/e2e-wait-helmreleases.sh
clock=$(mktemp)
calls=$(mktemp)
printf '0\n' > "$clock"
date() { sed -n '1p' "$clock"; }
sleep() {
now=$(sed -n '1p' "$clock")
printf '%s\n' "$(( now + $1 ))" > "$clock"
}
kubectl() {
count=$(wc -l < "$calls")
printf '%s\n' "$*" >> "$calls"
if [ "$count" -eq 0 ]; then
echo 'temporary apiserver error' >&2
return 1
fi
helmrelease_snapshot 11
}
cozy_wait_all_helmreleases_ready 30 11 4 2 >/dev/null 2>&1
[ "$(sed -n '1p' "$clock")" -ge 6 ] || {
echo "the list error was mistaken for an all-Ready snapshot" >&2
exit 1
}
}
@test "a Ready but short release set keeps the gate closed" {
# The minimum-count half of the gate is what replaced the `wc -l` existence
# backstop removed from hack/e2e-install-cozystack.bats, and it is the only
# thing separating "everything is Ready" from "almost nothing got created":
# an install that produced three HelmReleases and stopped has every one of
# them Ready. Every other case here passes 11 or 12 items against a minimum
# of 11, so without this one the condition can be deleted outright and the
# suite stays green.
. hack/e2e-wait-helmreleases.sh
clock=$(mktemp)
printf '0\n' > "$clock"
date() { sed -n '1p' "$clock"; }
sleep() {
now=$(sed -n '1p' "$clock")
printf '%s\n' "$(( now + $1 ))" > "$clock"
}
kubectl() { helmrelease_snapshot 3; }
if cozy_wait_all_helmreleases_ready 30 11 4 2 >/dev/null 2>&1; then
echo "the gate opened on 3 Ready HelmReleases against a minimum of 11" >&2
exit 1
fi
}
@test "a failed read invalidates a stability window already in progress" {
# The list-error branch resets stable_fingerprint and stable_since, and the
# "transient list error" case above does not pin that: its read fails on the
# first call, when there is no window to invalidate. Reverting the reset left
# the suite green. Here the failure lands mid-window, so a gate that kept
# counting through it returns at the original start plus quiet_seconds
# instead of restarting the clock -- which would accept a set that was never
# observed stable across the gap.
. hack/e2e-wait-helmreleases.sh
clock=$(mktemp)
calls=$(mktemp)
printf '0\n' > "$clock"
date() { sed -n '1p' "$clock"; }
sleep() {
now=$(sed -n '1p' "$clock")
printf '%s\n' "$(( now + $1 ))" > "$clock"
}
kubectl() {
count=$(wc -l < "$calls")
printf 'call\n' >> "$calls"
# Ready on the first read, unreadable on the second, Ready again after:
# the window opens at t=0, is broken at t=2, and may only complete a
# full quiet period measured from the read that reopened it.
if [ "$count" -eq 1 ]; then
echo 'temporary apiserver error' >&2
return 1
fi
helmrelease_snapshot 11
}
cozy_wait_all_helmreleases_ready 60 11 6 2 >/dev/null 2>&1
if [ "$(sed -n '1p' "$clock")" -lt 10 ]; then
echo "the gate counted the broken read as part of the stability window" >&2
exit 1
fi
}
@test "a read slower than the poll interval is charged against the deadline" {
# `now` is sampled after the list read, not before it. Sampled before, a read
# that costs more than the poll interval is priced at zero and the loop buys
# another full iteration past the deadline -- so the ceiling the caller asked
# for is not one. Counted in kubectl calls: one snapshot plus the final
# diagnostic read is the whole run; a third call means a second poll happened
# after the deadline had already passed.
. hack/e2e-wait-helmreleases.sh
clock=$(mktemp)
calls=$(mktemp)
printf '0\n' > "$clock"
date() { sed -n '1p' "$clock"; }
sleep() {
now=$(sed -n '1p' "$clock")
printf '%s\n' "$(( now + $1 ))" > "$clock"
}
kubectl() {
printf 'call\n' >> "$calls"
now=$(sed -n '1p' "$clock")
printf '%s\n' "$(( now + 40 ))" > "$clock"
helmrelease_snapshot 11 1
}
cozy_wait_all_helmreleases_ready 30 11 4 2 >/dev/null 2>&1 || true
if [ "$(wc -l < "$calls")" -ne 2 ]; then
echo "expected one snapshot read and one diagnostic read, got $(wc -l < "$calls")" >&2
exit 1
fi
}
@test "install suite has one unmasked HelmRelease gate" {
[ "$(grep -c 'cozy_wait_all_helmreleases_ready 900 11 5 2' hack/e2e-install-cozystack.bats)" -eq 1 ]
if grep -q 'kubectl wait hr --all -A.*|| true' hack/e2e-install-cozystack.bats; then
echo "the old masked HelmRelease wait survived" >&2
exit 1
fi
}