mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-23 03:27:50 +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>
333 lines
14 KiB
Bash
333 lines
14 KiB
Bash
#!/usr/bin/env bats
|
|
# -----------------------------------------------------------------------------
|
|
# Unit tests for the .PHONY declarations this tree depends on: the one in
|
|
# hack/package.mk, which most package Makefiles under packages/ include, and the
|
|
# root Makefile's, where one target is already shadowed by a real path.
|
|
#
|
|
# `.PHONY=a b c` assigns a variable; `.PHONY: a b c` declares targets. Only the
|
|
# second does anything, and make reports neither. The declaration lists the nine
|
|
# targets package.mk defines and stops there.
|
|
#
|
|
# `update` and `image` are deliberately absent: package.mk does not define them,
|
|
# the including package Makefile does, and naming an undefined target in .PHONY
|
|
# creates it as an empty target. That turns "No rule to make target" (exit 2)
|
|
# into "Nothing to be done" (exit 0) wherever the rule is missing, including the
|
|
# CI build matrix, which is parsed from the root Makefile `build:` target and
|
|
# runs `make -C <pkg> image` per unit. The last two tests pin the exclusion.
|
|
#
|
|
# The first test and the control below deliberately read different lists — the
|
|
# first the names on the `.PHONY:` line, the control the targets the file
|
|
# defines — and merging them back into one is the tidy-up to resist. Which list
|
|
# each takes is argued where it is taken.
|
|
#
|
|
# The first test is narrower than its name suggests, in two ways. It probes the
|
|
# names on the `.PHONY:` line, so it cannot see a name dropped from that line;
|
|
# the set-equality test is what guards every name's presence. And it decides by
|
|
# matching make's "is up to date", which make prints only for a target that has
|
|
# a recipe. A recipe-less alias never draws it: with no prerequisite both forms
|
|
# print "Nothing to be done", and with a prerequisite that is itself out of
|
|
# date, which every target here is for being phony, the working form prints that
|
|
# prerequisite's recipe while the broken form prints "Nothing to be done". The
|
|
# probe separates neither pair, so it would pass on such a name either way.
|
|
# Every target package.mk defines has a recipe, so nothing is uncovered today.
|
|
# The control test below does not close the gap either: an alias is a defined
|
|
# target, so the control probes it and requires "is up to date" from it, which
|
|
# turns the control red against the probe rather than against the alias.
|
|
# Adding one means revisiting both.
|
|
#
|
|
# The decoy files all carry one fixed timestamp, and that is load-bearing rather
|
|
# than tidy. Six of the nine targets take `check` as a prerequisite, and `apply`
|
|
# and `delete` reach it through `suspend` as well. Touch the decoys in list order
|
|
# and `check` lands newer than the targets that depend on it, so make rebuilds
|
|
# them for that reason alone and prints a recipe whatever `.PHONY` says. The
|
|
# probe then reports success against the broken form. Equal mtimes remove the
|
|
# question: nothing is ever newer than its own prerequisite, in any list order,
|
|
# so the recipe running means phony and nothing else. Measured on GNU Make 4.3,
|
|
# which is what CI runs, list order hid the bug for up to six of the nine, and
|
|
# which six moved with where the clock tick fell. On 3.81 the whole loop landed
|
|
# inside one timestamp and hid nothing, so a local run could not see it.
|
|
#
|
|
# Constraints on anyone editing this file: every make invocation is pinned to
|
|
# LC_ALL=C, because the assertions match English make wording; cozytest.sh
|
|
# recognizes only @test blocks and a bare `}` at column 0, and injects `set -e`,
|
|
# so exit statuses are captured explicitly rather than tested inline.
|
|
#
|
|
# Run with: bats hack/package-mk-phony.bats
|
|
# -----------------------------------------------------------------------------
|
|
|
|
load test_helper
|
|
|
|
@test "every target hack/package.mk declares phony still runs when a file of that name exists" {
|
|
repo=$(pwd)
|
|
targets=$(sed -n 's/^\.PHONY:[[:space:]]*//p' "$repo/hack/package.mk")
|
|
|
|
# Without this guard the loop below is a vacuous pass against `.PHONY=`,
|
|
# which the sed no longer matches.
|
|
if [ -z "$targets" ]; then
|
|
echo "no '.PHONY:' declaration found in hack/package.mk" >&2
|
|
exit 1
|
|
fi
|
|
|
|
tmp=$(mktemp -d)
|
|
printf 'include %s/hack/package.mk\n' "$repo" > "$tmp/Makefile"
|
|
|
|
# One fixed timestamp for every decoy, so no target is ever newer than a
|
|
# target it depends on. See the header: list order silently disarms this
|
|
# probe for the targets that hang off `check`.
|
|
for t in $targets; do
|
|
touch -t 200101010000 "$tmp/$t"
|
|
done
|
|
|
|
for t in $targets; do
|
|
# cozytest.sh runs with `set -e`, so capture the exit status explicitly
|
|
# rather than letting a failing make abort before the diagnostics below.
|
|
rc=0
|
|
out=$(LC_ALL=C make --dry-run --directory "$tmp" "$t" 2>&1) || rc=$?
|
|
if [ "$rc" -ne 0 ]; then
|
|
echo "make $t exited $rc under --dry-run:" >&2
|
|
echo "$out" >&2
|
|
rm -rf "$tmp"
|
|
exit 1
|
|
fi
|
|
case "$out" in
|
|
*"is up to date"*)
|
|
echo "make $t no-oped with a file named $t present:" >&2
|
|
echo "$out" >&2
|
|
echo "hack/package.mk does not actually declare $t phony" >&2
|
|
rm -rf "$tmp"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the probe reports up-to-date when .PHONY is written as an assignment" {
|
|
# Negative control: proves the probe can see a file-backed target at all.
|
|
# The list is the targets package.mk defines, not the names on its `.PHONY:`
|
|
# line, because "is up to date" is only a meaningful answer for a name that
|
|
# has a rule. A declared name the file does not define prints "Nothing to be
|
|
# done" in both forms, so probing it here fails against the probe instead of
|
|
# against the defect; the set-equality test is what catches that name, and
|
|
# it carries the message that says so. The list is read from the original
|
|
# file: the copy is the mutated one.
|
|
repo=$(pwd)
|
|
targets=$(grep -E '^[A-Za-z0-9][A-Za-z0-9_-]*([[:space:]]+[A-Za-z0-9][A-Za-z0-9_-]*)*:' \
|
|
"$repo/hack/package.mk" \
|
|
| grep -vE '^[^:]*:+=' | sed 's/:.*//' \
|
|
| tr ' ' '\n' | grep -v '^$' | sort -u)
|
|
if [ -z "$targets" ]; then
|
|
echo "no targets extracted from hack/package.mk — extraction broke" >&2
|
|
exit 1
|
|
fi
|
|
|
|
tmp=$(mktemp -d)
|
|
|
|
sed 's/^\.PHONY:/.PHONY=/' "$repo/hack/package.mk" > "$tmp/package.mk"
|
|
if ! grep -q '^\.PHONY=' "$tmp/package.mk"; then
|
|
echo "mutation to the assignment form did not apply to the copy" >&2
|
|
rm -rf "$tmp"
|
|
exit 1
|
|
fi
|
|
|
|
printf 'include %s/package.mk\n' "$tmp" > "$tmp/Makefile"
|
|
|
|
# Same fixed timestamp as the first test, for the same reason.
|
|
for t in $targets; do
|
|
touch -t 200101010000 "$tmp/$t"
|
|
done
|
|
|
|
for t in $targets; do
|
|
rc=0
|
|
out=$(LC_ALL=C make --dry-run --directory "$tmp" "$t" 2>&1) || rc=$?
|
|
if [ "$rc" -ne 0 ]; then
|
|
echo "make $t exited $rc under --dry-run:" >&2
|
|
echo "$out" >&2
|
|
rm -rf "$tmp"
|
|
exit 1
|
|
fi
|
|
case "$out" in
|
|
*"is up to date"*) ;;
|
|
*)
|
|
echo "probe failed to detect a file-backed $t:" >&2
|
|
echo "$out" >&2
|
|
rm -rf "$tmp"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the .PHONY list and the targets package.mk defines are the same set" {
|
|
# The first test only probes names already on the list, so this is the sole
|
|
# guard on a name being dropped from it. The reverse direction catches a
|
|
# declared-but-undefined name, which make turns into an empty target.
|
|
# `%-update` is out of reach for both: .PHONY matches literally.
|
|
repo=$(pwd)
|
|
# Flattened because the `case` tests match on " name " and an embedded
|
|
# newline makes a name fail that check and get reported wrongly.
|
|
declared=$(sed -n 's/^\.PHONY:[[:space:]]*//p' "$repo/hack/package.mk" \
|
|
| tr '\n' ' ')
|
|
# A target name outside this pattern — leading underscore, dot, slash — is
|
|
# legal in make and skipped here in silence, so keep new names inside it.
|
|
# One rule may name several targets, so the pattern accepts a run of them
|
|
# and the `tr` splits the run apart again; anchoring the colon to a single
|
|
# name instead would drop every name on such a line, silently for a target
|
|
# nothing depends on. The second grep drops assignments by the `:=` itself
|
|
# rather than by whatever precedes it, `:+` covering `::=` and `:::=`, so
|
|
# `export FOO:=bar` goes too — matching the name in front instead would let
|
|
# that line through and report `export` and `FOO` as undeclared targets.
|
|
defined=$(grep -E '^[A-Za-z0-9][A-Za-z0-9_-]*([[:space:]]+[A-Za-z0-9][A-Za-z0-9_-]*)*:' \
|
|
"$repo/hack/package.mk" \
|
|
| grep -vE '^[^:]*:+=' | sed 's/:.*//' \
|
|
| tr ' ' '\n' | grep -v '^$' | sort -u | tr '\n' ' ')
|
|
|
|
if [ -z "$declared" ] || [ -z "$defined" ]; then
|
|
echo "could not extract targets from hack/package.mk — extraction broke" >&2
|
|
exit 1
|
|
fi
|
|
|
|
missing=""
|
|
for t in $defined; do
|
|
case " $declared " in
|
|
*" $t "*) ;;
|
|
*) missing="$missing $t" ;;
|
|
esac
|
|
done
|
|
|
|
if [ -n "$missing" ]; then
|
|
echo "hack/package.mk defines these targets but does not declare them phony:$missing" >&2
|
|
echo "a file of that name next to a package Makefile would silence the recipe" >&2
|
|
exit 1
|
|
fi
|
|
|
|
undefined=""
|
|
for t in $declared; do
|
|
case " $defined " in
|
|
*" $t "*) ;;
|
|
*) undefined="$undefined $t" ;;
|
|
esac
|
|
done
|
|
|
|
if [ -n "$undefined" ]; then
|
|
echo "hack/package.mk declares these targets phony but defines none of them:$undefined" >&2
|
|
echo "naming an undefined target in .PHONY creates it as an empty target, so the" >&2
|
|
echo "command reports success instead of failing with 'No rule to make target'" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
@test "update and image stay undeclared so a package without the rule fails loudly" {
|
|
# Pins the exclusion: a package with no `update:`/`image:` rule must still
|
|
# fail loudly. Declaring them centrally would make every such package report
|
|
# success having built nothing, CI included.
|
|
repo=$(pwd)
|
|
tmp=$(mktemp -d)
|
|
|
|
printf 'include %s/hack/package.mk\n' "$repo" > "$tmp/Makefile"
|
|
|
|
for t in update image; do
|
|
rc=0
|
|
out=$(LC_ALL=C make --directory "$tmp" "$t" 2>&1) || rc=$?
|
|
# Exit status and diagnostic are independent observables, so both are
|
|
# pinned. 2 is what make returns for a fatal error, and the number is
|
|
# what a caller branches on — a wrapper or a future make that kept the
|
|
# wording while returning something else would pass the case below.
|
|
if [ "$rc" -ne 2 ]; then
|
|
echo "expected 'make $t' to exit 2 in a package that defines no $t rule," >&2
|
|
echo "but it exited $rc — is $t named in the .PHONY list?" >&2
|
|
echo "$out" >&2
|
|
rm -rf "$tmp"
|
|
exit 1
|
|
fi
|
|
case "$out" in
|
|
*"No rule to make target"*) ;;
|
|
*)
|
|
echo "expected 'make $t' to report no rule to make target, got:" >&2
|
|
echo "$out" >&2
|
|
rm -rf "$tmp"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "adding update and image to the .PHONY list silences that failure" {
|
|
# Negative control. Test 4 passes vacuously if its Makefile never reads
|
|
# package.mk at all: a broken `include` path also exits non-zero with "No
|
|
# rule to make target", naming the missing file. This mutates a copy and
|
|
# greps that the mutation landed, so a wrong path fails here instead.
|
|
repo=$(pwd)
|
|
tmp=$(mktemp -d)
|
|
|
|
sed 's/^\(\.PHONY:.*\)$/\1 update image/' "$repo/hack/package.mk" > "$tmp/package.mk"
|
|
if ! grep -q '^\.PHONY:.* update image$' "$tmp/package.mk"; then
|
|
echo "mutation extending the declaration did not apply to the copy" >&2
|
|
rm -rf "$tmp"
|
|
exit 1
|
|
fi
|
|
|
|
printf 'include %s/package.mk\n' "$tmp" > "$tmp/Makefile"
|
|
|
|
for t in update image; do
|
|
rc=0
|
|
out=$(LC_ALL=C make --directory "$tmp" "$t" 2>&1) || rc=$?
|
|
if [ "$rc" -ne 0 ]; then
|
|
echo "mutation did not take effect: 'make $t' still failed with exit $rc" >&2
|
|
echo "$out" >&2
|
|
rm -rf "$tmp"
|
|
exit 1
|
|
fi
|
|
case "$out" in
|
|
*"Nothing to be done"*) ;;
|
|
*)
|
|
echo "expected the mutated copy to report nothing to be done for $t, got:" >&2
|
|
echo "$out" >&2
|
|
rm -rf "$tmp"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the root Makefile declares test phony so the test directory cannot shadow it" {
|
|
# The one target in this tree that a real path already shadows. `test:` in
|
|
# the root Makefile produces no file, so make treats the existing test/
|
|
# directory as its product: undeclared, `make test` prints "is up to date"
|
|
# and the recipe never runs, while docs/agents/overview.md advertises the
|
|
# command as the way to run the full suite. Every other undeclared target
|
|
# in that file is unshadowed today, so this is the only live one.
|
|
#
|
|
# Probed with --dry-run because the recipe wants a live cluster. The path is
|
|
# what makes the probe mean anything, so its absence fails loudly rather
|
|
# than passing quietly.
|
|
repo=$(pwd)
|
|
if [ ! -e "$repo/test" ]; then
|
|
echo "no test/ path at the repo root: this probe no longer measures anything" >&2
|
|
exit 1
|
|
fi
|
|
|
|
rc=0
|
|
out=$(LC_ALL=C make --dry-run --directory "$repo" test 2>&1) || rc=$?
|
|
if [ "$rc" -ne 0 ]; then
|
|
echo "make test exited $rc under --dry-run:" >&2
|
|
echo "$out" >&2
|
|
exit 1
|
|
fi
|
|
case "$out" in
|
|
*"is up to date"*)
|
|
echo "make test no-oped with the test/ path present:" >&2
|
|
echo "$out" >&2
|
|
echo "the root Makefile does not declare test phony" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|