Files
cozystack/hack/helmrelease-readiness.bats
T
Myasnikov Daniil 8456facc79 test(e2e): pin the stability-window reset and keep the fan-out test portable
The readiness gate resets its stability window when a list read fails,
and nothing held that: the existing "transient list error" case fails on
the first read, when there is no window to invalidate, so removing the
reset left the suite green. The new case breaks a read mid-window, where
a gate that kept counting through the gap would accept a release set it
never observed stable across it.

The parallel fan-out contract asked make for --output-sync, which arrived
in GNU Make 4.0 while macOS still ships 3.81 -- so on a contributor's
machine the suite died on an unknown option instead of reporting the
contract. Guarded by version, with the reason in place. CI runs 4.x, so
the contract is still enforced where it gates a merge.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Assisted-By: LLM
2026-09-07 16:33:27 +05:00

219 lines
7.7 KiB
Bash

#!/usr/bin/env bats
# Unit coverage for the single post-install HelmRelease readiness gate.
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
}