mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-03 02:27:01 +09:00
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>
206 lines
9.9 KiB
Bash
206 lines
9.9 KiB
Bash
#!/usr/bin/env bats
|
|
# -----------------------------------------------------------------------------
|
|
# Behavioural tests for the cluster-capture gate in hack/cozytest.sh.
|
|
#
|
|
# The runner arms its on-failure cluster captures from an EXIT trap. Those
|
|
# captures belong to the e2e suites, which run against a live cluster; a unit
|
|
# suite has no cluster to snapshot, so arming them there buys an empty capture
|
|
# and pays the capture's timeouts for it. The gate therefore keys on which kind
|
|
# of suite is running rather than on whether a kubectl binary happens to be
|
|
# installed, because a unit runner has the binary and no cluster.
|
|
#
|
|
# Both directions are asserted, and they share one fixture generator that
|
|
# differs only in the fixture's filename. That pairing is the point: the
|
|
# unit-side assertion is a negative one, and a negative assertion passes just as
|
|
# well when the harness is broken and nothing ran at all. The e2e-side test is
|
|
# the positive control for exactly that failure, and the unit-side test
|
|
# additionally pins that the run did fail, so "no captures" cannot be satisfied
|
|
# by "no test".
|
|
#
|
|
# The stub kubectl is what makes this a real test of the gate: with no kubectl
|
|
# on PATH every capture leg is skipped for a second reason and both directions
|
|
# would agree for the wrong cause.
|
|
#
|
|
# awk-transform constraint (hack/cozytest.sh rewrites this file before sourcing
|
|
# it): it turns any line beginning `@test "` into a function header, and any
|
|
# line that is exactly `}` into `return 0` plus the brace. Neither rule knows
|
|
# what a heredoc is. So a fixture written as a heredoc here has its own `@test`
|
|
# line harvested into THIS suite: the runner registers a phantom extra test,
|
|
# runs it first, and the fixture's `false` ends the run before any real test
|
|
# executes. Checked by building that variant rather than reasoned about -- it
|
|
# fails loudly rather than silently, but it fails the wrong thing, and the gate
|
|
# is then covered by nothing. The fixtures are built with printf, which puts the
|
|
# `@test` text inside a format string where no rule matches it.
|
|
#
|
|
# Title syntax constraints (also inherited from that parser):
|
|
# - Titles delimited by ASCII double quotes; embedded quotes truncate.
|
|
# - Only [A-Za-z0-9] from the title survives into the function name, so keep
|
|
# titles distinctive in their alphanumeric run.
|
|
#
|
|
# Run with: bats hack/cozytest-capture-gate.bats
|
|
# -----------------------------------------------------------------------------
|
|
|
|
load test_helper
|
|
|
|
HACK_DIR="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")" && pwd)"
|
|
RUNNER="$HACK_DIR/cozytest.sh"
|
|
|
|
# Marker printed by the runner's data-plane capture leg. Chosen as one probe
|
|
# because the runner echoes it before invoking the capture, so the assertion
|
|
# reads the gate's decision rather than the capture's outcome.
|
|
DATAPLANE_MARKER='capturing host->pod data-plane'
|
|
|
|
# The gate covers three legs, and each leaves a distinct artefact under the
|
|
# snapshot directory. Pinning all three from the report tree is what keeps any
|
|
# one of them from being ungated without a test noticing: the echo above covers
|
|
# only the data-plane leg, and the crust-gather leg is the one whose cost is
|
|
# worst, since it needs no reachable cluster to fire, only an ambient
|
|
# KUBECONFIG.
|
|
assert_legs_armed() {
|
|
_rdir=$1 _name=$2
|
|
[ -f "$_rdir/snapshots/$_name/crust-gather.log" ]
|
|
[ -d "$_rdir/snapshots/$_name/dataplane" ]
|
|
[ -f "$_rdir/snapshots/$_name/previous-logs/capture-notes.txt" ]
|
|
}
|
|
|
|
assert_legs_disarmed() {
|
|
_rdir=$1 _name=$2
|
|
for _leg in crust-gather.log dataplane previous-logs; do
|
|
if [ -e "$_rdir/snapshots/$_name/$_leg" ]; then
|
|
echo "cluster capture leg $_leg fired for a unit suite"
|
|
exit 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Build a scratch dir holding stub cluster tools and a single-test fixture whose
|
|
# test fails. $1 is the fixture's path relative to the scratch dir, which is the
|
|
# only thing that differs between the directions under test.
|
|
#
|
|
# Both kubectl and crust-gather are stubbed, and crust-gather is not optional:
|
|
# the runner invokes it directly rather than through a script that needs
|
|
# kubectl output first, so a kubectl-only stub leaves that leg reaching for the
|
|
# real binary against the ambient KUBECONFIG. On a machine that has both -- a
|
|
# maintainer's laptop, the e2e sandbox image -- `make unit-tests` would then
|
|
# snapshot whatever cluster happens to be current, for up to the leg's 390s.
|
|
# That is the same bill this change exists to stop paying.
|
|
make_fixture_dir() {
|
|
_dir=$(mktemp -d)
|
|
mkdir -p "$_dir/bin" "$_dir/$(dirname "$1")"
|
|
for _tool in kubectl crust-gather; do
|
|
printf '#!/bin/sh\nexit 0\n' >"$_dir/bin/$_tool"
|
|
chmod +x "$_dir/bin/$_tool"
|
|
done
|
|
printf '@test "fixture fails on purpose" {\n false\n}\n' >"$_dir/$1"
|
|
printf '%s' "$_dir"
|
|
}
|
|
|
|
# Run the runner against a fixture with the stub kubectl first on PATH and the
|
|
# report directory redirected into the scratch dir, so a capture that does fire
|
|
# writes there instead of into the worktree. The output goes to a file rather
|
|
# than a variable on purpose: these suites run under `set -x`, and a variable
|
|
# holding the fixture's output gets expanded into the trace of whatever command
|
|
# reads it, printing the fixture's deliberate "Test failed" line into the job
|
|
# log where it reads as a real failure.
|
|
run_fixture() {
|
|
_fdir=$1 _fname=$2
|
|
PATH="$_fdir/bin:$PATH" COZY_REPORT_DIR="$_fdir/report" \
|
|
"$RUNNER" "$_fdir/$_fname" >"$_fdir/out" 2>&1 || true
|
|
}
|
|
|
|
@test "an e2e suite failing still arms the cluster captures" {
|
|
dir=$(make_fixture_dir e2e-gate-fixture.bats)
|
|
run_fixture "$dir" e2e-gate-fixture.bats
|
|
# Positive control for the negative assertion in the last test: same fixture,
|
|
# same stubs, e2e filename.
|
|
grep -qF "$DATAPLANE_MARKER" "$dir/out"
|
|
# The capture ran to its own end here -- the stub kubectl answers instantly --
|
|
# so the incompleteness line the next test pins must NOT appear. Without this
|
|
# the same line would satisfy that test whether it tracked the leg's status or
|
|
# was printed unconditionally.
|
|
if grep -qF 'data-plane capture INCOMPLETE' "$dir/out"; then
|
|
echo "a capture that finished was reported as cut short:"
|
|
cat "$dir/out"
|
|
exit 1
|
|
fi
|
|
assert_legs_armed "$dir/report" e2e-gate-fixture
|
|
rm -rf "$dir"
|
|
}
|
|
|
|
@test "a data-plane capture cut short by its backstop says so" {
|
|
# The collector names every read of its own that could not finish, which
|
|
# leaves the outer backstop here as the one way its leg can still die without
|
|
# a word -- and a truncated dataplane/ then reads exactly like a complete one
|
|
# that found little. The sibling previous-logs leg has reported this since it
|
|
# was added; this is the same discipline at the third leg.
|
|
#
|
|
# The backstop is simulated with a `timeout` on PATH that exits 124 rather
|
|
# than by making the collector overrun a real 600s bound. The call site
|
|
# cannot tell the difference: it sees a non-zero status from `timeout` and
|
|
# whatever had been written before the kill, which is the whole input to the
|
|
# behaviour under test.
|
|
dir=$(make_fixture_dir e2e-backstop-fixture.bats)
|
|
printf '#!/bin/sh\nexit 124\n' >"$dir/bin/timeout"
|
|
chmod +x "$dir/bin/timeout"
|
|
run_fixture "$dir" e2e-backstop-fixture.bats
|
|
# Positive control: the leg was armed and reached, so the assertion below is
|
|
# about what the leg said and not about the gate having skipped it.
|
|
grep -qF "$DATAPLANE_MARKER" "$dir/out"
|
|
grep -qF 'data-plane capture INCOMPLETE (exit 124)' "$dir/out"
|
|
rm -rf "$dir"
|
|
}
|
|
|
|
@test "a data-plane collector that is not there is not reported as a capture" {
|
|
# This leg is gated on kubectl alone, while the previous-logs leg beside it
|
|
# requires its collector to be executable and so does the Chainsaw caller.
|
|
# The asymmetry cost nothing while the status was discarded; now that a
|
|
# non-zero status prints "kept what landed in <dir>", a tree without the
|
|
# collector claims a partial capture in a directory nothing ever created.
|
|
dir=$(make_fixture_dir e2e-nocollector-fixture.bats)
|
|
# The runner resolves both collectors relative to its own path, so copying it
|
|
# somewhere without its siblings is what makes them absent -- nothing in the
|
|
# worktree is touched.
|
|
mkdir -p "$dir/hack"
|
|
cp "$RUNNER" "$dir/hack/cozytest.sh"
|
|
PATH="$dir/bin:$PATH" COZY_REPORT_DIR="$dir/report" \
|
|
"$dir/hack/cozytest.sh" "$dir/e2e-nocollector-fixture.bats" >"$dir/out" 2>&1 || true
|
|
# Positive controls: the fixture failed, so the handler ran, and the gate
|
|
# armed -- otherwise the absence below would be satisfied by nothing running.
|
|
grep -qF 'Test failed' "$dir/out"
|
|
[ -f "$dir/report/snapshots/e2e-nocollector-fixture/crust-gather.log" ]
|
|
if grep -qF 'data-plane capture INCOMPLETE' "$dir/out"; then
|
|
echo "a collector that was never there was reported as a capture cut short:"
|
|
cat "$dir/out"
|
|
exit 1
|
|
fi
|
|
rm -rf "$dir"
|
|
}
|
|
|
|
@test "a live cluster suite under an e2e directory keeps its captures" {
|
|
# The pre-Chainsaw suites under hack/e2e-apps/ used plain filenames. This
|
|
# fixture pins the directory-based guard so that layout retains captures if it
|
|
# returns, even though no live suite currently uses it.
|
|
dir=$(make_fixture_dir e2e-apps/plain-named-suite.bats)
|
|
run_fixture "$dir" e2e-apps/plain-named-suite.bats
|
|
grep -qF "$DATAPLANE_MARKER" "$dir/out"
|
|
assert_legs_armed "$dir/report" plain-named-suite
|
|
rm -rf "$dir"
|
|
}
|
|
|
|
@test "a unit suite failing does not arm the cluster captures" {
|
|
dir=$(make_fixture_dir plain-gate-fixture.bats)
|
|
run_fixture "$dir" plain-gate-fixture.bats
|
|
# The run must have failed, or "no captures" would be satisfied by "no test".
|
|
grep -qF 'Test failed' "$dir/out"
|
|
# Spelled as an if rather than `! grep ...`: `set -e` is specified to ignore a
|
|
# command whose status is inverted with `!`, so the negated form would report
|
|
# success even when the marker is present, which is the whole thing this test
|
|
# exists to catch.
|
|
if grep -qF "$DATAPLANE_MARKER" "$dir/out"; then
|
|
echo "cluster captures were armed for a unit suite"
|
|
exit 1
|
|
fi
|
|
assert_legs_disarmed "$dir/report" plain-gate-fixture
|
|
rm -rf "$dir"
|
|
}
|