Files
cozystack/hack/select-e2e_test.bats
Aleksei Sviridkin d2aa5f68f7 test(e2e): add a GPUDevice suite on a fake extended resource
Nothing exercised GPUDevice end to end: ordering and binding, budget
and capacity refusals at admission, attachment to a VMI with the
launcher pod pinned to the device's node, the usage record, template
devices that live with their VMI, and deletion of a tenant still
holding a device. CI has no GPUs, so the suite advertises two units
of a fake extended resource on one node and restores the node at
cleanup.

No device plugin serves the fake name, so virt-handler never hands
the guest a device and the VMI does not sync. The VM checks stop at
what admission and scheduling decide; the guest side stays manual.

The suite creates VMs on a VMDisk, so both VM application sources and
gpu-operator select it, and its install closure keeps gpu-operator.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-22 11:35:09 +03:00

1315 lines
64 KiB
Bash

#!/usr/bin/env bats
# -----------------------------------------------------------------------------
# Unit tests for hack/select-e2e.sh
#
# CI runs this file under Bats through `make bats-unit-tests`. It also remains
# compatible with the legacy `hack/cozytest.sh` translator, whose awk parser
# recognizes only @test blocks and a bare `}` on its own line. The tests avoid
# `run`, `$status`, setup(), and teardown() for that compatibility path and use
# direct shell assertions with inline fixture cleanup.
#
# Test-level EXIT traps replace Bats' own handler and hide failing TAP results.
# Cleanup follows aborting assertions; see docs/agents/e2e-testing.md.
#
# Run with: bats hack/select-e2e_test.bats
# -----------------------------------------------------------------------------
load test_helper
# Assert that a selection is the WHOLE suite list, not merely a long one.
#
# These asserts used to read `[ "$(echo "$output" | wc -w)" -gt 5 ]`, which is
# satisfied by any selection of six or more suites. The regression worth
# catching on every escalation path is partial escalation — a selector bug that
# picks most suites but not all — and a threshold cannot see it: at the tree's
# suite count, dropping most of them still passes. Equality can.
#
# The expected set is derived the way the script's full-suite branch derives it
# rather than pinned as a literal, so adding or disabling a Chainsaw suite does
# not need an edit at every full-suite assertion here.
#
# A helper rather than an inline one-liner because cozytest.sh runs each @test
# under `set -x`: a bare failing `[ ... ]` prints the two values already
# expanded, but not which side is which, and reading a whole-tree diff off a trace
# line is exactly the moment a test stops being worth having.
full_suite_list() {
find hack/e2e-chainsaw -mindepth 2 \( -not -type d -o -name chainsaw-test.yaml -o -name chainsaw-test.yml \) \
| sed -nE 's,^hack/e2e-chainsaw/([^/]+)/(.*/)?(chainsaw-test|[0-9]+-[^/]*)\.ya?ml$,\1,p' \
| sort -u | paste -sd ' ' -
}
assert_selection() {
if [ "$2" != "$3" ]; then
echo "$1" >&2
echo " want: $3" >&2
echo " got: $2" >&2
exit 1
fi
}
assert_full_suite() {
assert_selection "expected the full Chainsaw suite" "$1" "$(full_suite_list)"
}
@test "single app diff selects only that suite" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "packages/apps/postgres/values.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "postgres" ]
rm -rf "$tmp"
}
@test "operator diff selects all dependent app suites" {
# postgres-operator is depended on by postgres-application, harbor-application
# (Harbor uses postgres as its backing DB), and monitoring-application (Grafana
# DB), each of which has a chainsaw suite.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "packages/system/postgres-operator/values.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
echo "$output" | grep -wq postgres
echo "$output" | grep -wq harbor
if echo "$output" | grep -wq kafka; then
echo "operator diff must not trigger full suite; got: $output" >&2
exit 1
fi
rm -rf "$tmp"
}
@test "engine-dependency change does not fan out via the ordering edge" {
# cert-manager is a dependency of cozystack-engine, and every app declares
# dependsOn cozystack-engine purely as an INSTALL-ORDERING edge (the app's
# *-rd HelmRelease waits for the ApplicationDefinition CRD). That edge must
# not propagate test selection: a cert-manager change selects only its
# genuine direct dependents (postgres, harbor, ...), never unrelated apps
# like kafka that reach cert-manager solely through the engine.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "packages/system/cert-manager/values.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
echo "$output" | grep -wq postgres
echo "$output" | grep -wq harbor
if echo "$output" | grep -wq kafka; then
echo "cert-manager change must not fan out via engine; got: $output" >&2
exit 1
fi
rm -rf "$tmp"
}
@test "cozystack-basics does not narrow to a suite that lands downstream of it" {
# The other install-ordering hub. Its edges exist so a namespace or a
# platform-wide policy is in place first, and it reaches no suite today, so
# it escalates. The hazard is what happens when a suite DOES land downstream:
# the walk would carry cozystack-basics into it and the platform's
# namespace-and-policy package would run that one suite instead of
# everything. Seeded here rather than waited for, by giving an app that owns
# a real suite an edge onto the hub — the shape #3426 produces for real,
# where enabling the site-router suite takes cozystack-basics from the full
# run to `site-router` alone.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
yq -i '.spec.variants[0].dependsOn += ["cozystack.cozystack-basics"]' \
"$tmp/sources/redis-application.yaml"
# Premise: the seeded edge is the thing under test, so a redis change must
# still select redis. Without it the assertion below passes on an empty
# graph.
echo "packages/apps/redis/values.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
echo "$output" | grep -wq redis
echo "packages/system/cozystack-basics/values.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_full_suite "$output"
rm -rf "$tmp"
}
@test "a CDI change selects a closure that keeps VMDisk and CDI" {
# kubevirt-cdi reaches vm-disk-application and nothing else runnable, so
# until vm-disk-application mapped to a suite every CDI change ran all of
# them. The vminstance suite creates a VMDisk and asserts the DataVolume
# behind it, so it is the suite that covers CDI; gpudevice attaches a
# VMDisk to its VMs too.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "packages/system/kubevirt-cdi/values.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "gpudevice vminstance" ]
closure=$(hack/select-install.sh "$output" "$tmp/sources")
printf '%s\n' "$closure" | tr ' ' '\n' | grep -Fxq cozystack.vm-disk-application
printf '%s\n' "$closure" | tr ' ' '\n' | grep -Fxq cozystack.kubevirt-cdi
rm -rf "$tmp"
}
@test "a CNI change selects every suite the graph can reach" {
# Named for what it measures rather than for the full suite it once claimed.
# `packages/system/cilium` is owned by cozystack.networking and resolves
# through the dependency graph, not through full_suite_pattern, so what this
# measures is "every suite reachable from networking".
#
# That used to be 19 of the 21 suites, subtracted here by name: the mapping
# covered *-application sources plus external-dns, so the walk reached
# cozystack.kuberture and cozystack.securitygroup-controller and the filter
# dropped them again — a CNI change ran neither of the two controller suites
# most likely to regress from it. With the mapping no longer restricted to
# app-shaped names, both round-trip and the reachable set is the whole tree.
# The subtraction is gone rather than inverted: leaving it as an exclusion
# list would keep the assertion green if the walk stopped reaching them for
# some unrelated reason.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
script="$PWD/hack/select-e2e.sh"
echo "packages/system/cilium/values.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_selection "expected every graph-reachable suite" "$output" "$(full_suite_list)"
# Reachable and full now agree, so the equality above can no longer tell a
# walk that reached everything from an escalation that printed everything.
# Re-run against a tree holding one suite networking reaches and one no
# source names at all: only a real walk leaves the second out, and an
# escalation prints both.
mkdir -p "$tmp/tree/hack/e2e-chainsaw/kuberture" "$tmp/tree/hack/e2e-chainsaw/zz-no-source"
touch "$tmp/tree/hack/e2e-chainsaw/kuberture/chainsaw-test.yaml" \
"$tmp/tree/hack/e2e-chainsaw/zz-no-source/chainsaw-test.yaml"
scoped=$(cd "$tmp/tree" && "$script" "$tmp/diff" "$tmp/sources")
assert_selection "a CNI change must reach kuberture through the graph, not by escalating" \
"$scoped" "kuberture"
rm -rf "$tmp"
}
@test "library change triggers full suite" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "packages/library/cozy-lib/templates/_helpers.tpl" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources" 2>"$tmp/err")
assert_full_suite "$output"
# full_suite_pattern is the commonest escalation cause by a wide margin, so a
# full run with nothing on stderr leaves the usual answer to "why did
# everything run" out of the log entirely. Matched on the path and the rule
# name rather than on the word "escalating", which every other reason line
# here also prints.
if ! grep -q "select-e2e:.*cozy-lib.*full_suite_pattern" "$tmp/err"; then
echo "a full_suite_pattern match must name the path that caused it; stderr was:" >&2
cat "$tmp/err" >&2
exit 1
fi
rm -rf "$tmp"
}
@test "docs-only diff selects nothing" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "docs/README.md" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ -z "$output" ]
rm -rf "$tmp"
}
@test "kubernetes-application maps to the two kubernetes suites" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "packages/apps/kubernetes/values.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
echo "$output" | grep -q "kubernetes-latest"
echo "$output" | grep -q "kubernetes-previous"
# OIDC System -> CustomConfig lifecycle coverage is folded into latest.
if echo "$output" | grep -q "kubernetes-oidc-"; then
echo "FAIL: selector still names the removed OIDC suites" >&2
false
fi
rm -rf "$tmp"
}
@test "dashboards-only diff selects nothing (path is plural)" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "dashboards/gpu/gpu-fleet.json" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ -z "$output" ]
rm -rf "$tmp"
}
@test "shared E2E helper script triggers full suite" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "hack/e2e-chainsaw/_lib/run-kubernetes.sh" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources" 2>"$tmp/err")
assert_full_suite "$output"
if ! grep -q "select-e2e:.*run-kubernetes.sh.*shared by every Chainsaw suite" "$tmp/err"; then
echo "a shared-Chainsaw escalation must name the file that caused it; stderr was:" >&2
cat "$tmp/err" >&2
exit 1
fi
rm -rf "$tmp"
}
@test "chainsaw config change triggers full suite" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "hack/e2e-chainsaw/.chainsaw.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_full_suite "$output"
rm -rf "$tmp"
}
@test "install bats triggers full suite" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "hack/e2e-install-cozystack.bats" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_full_suite "$output"
rm -rf "$tmp"
}
@test "per-suite edit selects only that suite, never escalates" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "hack/e2e-chainsaw/redis/chainsaw-test.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "redis" ]
rm -rf "$tmp"
}
@test "pull-requests workflow change triggers full suite" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo ".github/workflows/pull-requests.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_full_suite "$output"
rm -rf "$tmp"
}
@test "backup example harness edit selects its app suite" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "examples/backups/postgres/run-all.sh" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "postgres" ]
rm -rf "$tmp"
}
@test "redis backup harness edit selects the valkey suite that also runs it" {
# valkey-backup-roundtrip drives examples/backups/redis with
# APP_KIND=Valkey, so a change to that harness must run both suites.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "examples/backups/redis/00-helpers.sh" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "redis valkey" ]
rm -rf "$tmp"
}
@test "a velero change selects the vminstance suite" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "packages/system/velero/values.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
rm -rf "$tmp"
case " $output " in *" vminstance "*) ;; *) echo "a velero change selected '$output'" >&2; exit 1 ;; esac
}
@test "vmi/vmdisk backup harness edit selects the vminstance suite" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
for f in examples/backups/vmi/run-all.sh examples/backups/vmdisk/run-all.sh; do
echo "$f" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "vminstance" ]
done
rm -rf "$tmp"
}
@test "vmi restore-to-copy step, which the suite skips, selects nothing" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "examples/backups/vmi/07-restore-to-copy.sh" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources") || true
rm -rf "$tmp"
[ -z "$output" ]
}
@test "backup example without a matching suite selects nothing" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "examples/backups/no-such-app/run.sh" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources") || true
[ -z "$output" ]
rm -rf "$tmp"
}
@test "a file directly under examples/backups escalates instead of selecting nothing" {
# A file beside the per-app directories rather than inside one belongs to
# no single walkthrough, so it is treated as shared by all of them. It
# matched neither backup rule and fell to examples/ in
# inert_config_pattern, so an edit to it selected nothing.
# README.md stays inert through the *.md rule, which runs first.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
for diff in "examples/backups/common.sh" \
"examples/backups/common.sh examples/backups/postgres/run-all.sh"; do
printf '%s\n' $diff > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources" 2>"$tmp/err")
assert_selection "a loose backup file was not escalated for: $diff" \
"$output" "$(full_suite_list)"
if ! grep -q "select-e2e: 'examples/backups/common.sh' belongs to no single backup walkthrough" "$tmp/err"; then
echo "the escalation must name the file; stderr was:" >&2
cat "$tmp/err" >&2
exit 1
fi
done
echo examples/backups/README.md > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ -z "$output" ]
rm -rf "$tmp"
}
@test "a shared backup helper escalates instead of selecting nothing" {
# examples/backups/_lib/ is sourced by the backup walkthroughs, so it is
# shared material the way hack/e2e-chainsaw/_lib/ is. Read as an app named
# _lib it matched no suite and selected nothing, which both lanes take as
# "skip Chainsaw": a change to every walkthrough at once would test none.
# Also pinned beside an app path, where selecting only that app would pass
# a test that checked for a non-empty selection.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
for diff in "examples/backups/_lib/wait-helpers.sh" \
"examples/backups/_lib/wait-helpers.sh examples/backups/postgres/run-all.sh"; do
printf '%s\n' $diff > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources" 2>"$tmp/err")
assert_selection "a shared backup helper was not escalated for: $diff" \
"$output" "$(full_suite_list)"
if ! grep -q "select-e2e: 'examples/backups/_lib/wait-helpers.sh' is shared by every backup walkthrough" "$tmp/err"; then
echo "the escalation must name the helper; stderr was:" >&2
cat "$tmp/err" >&2
exit 1
fi
done
rm -rf "$tmp"
}
# --- #3392: every path is classified; unclassified escalates -----------------
#
# The bug these cover: an unrecognised path used to select nothing, both lanes
# read an empty selection as "skip Chainsaw", and the required "E2E Tests"
# status was then posted green with no suite run. Each test below pins one side
# of the classification — escalate, or skip by an explicit rule.
@test "hack/*.mk triggers full suite (build flags of every image)" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "hack/common-envs.mk" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_full_suite "$output"
rm -rf "$tmp"
}
@test "the fork e2e workflow triggers full suite" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo ".github/workflows/e2e-fork.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_full_suite "$output"
rm -rf "$tmp"
}
@test "pkg/ triggers full suite (shipped Go code)" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "pkg/cozystack/registry.go" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_full_suite "$output"
rm -rf "$tmp"
}
@test "go.mod triggers full suite" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "go.mod" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_full_suite "$output"
rm -rf "$tmp"
}
@test "hack/lib helper triggers full suite" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "hack/lib/image-refs.sh" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_full_suite "$output"
rm -rf "$tmp"
}
@test "an unclassified path escalates instead of selecting nothing" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "brand-new-top-level/thing.conf" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources" 2>"$tmp/err")
assert_full_suite "$output"
# The reason line is asserted as well as the selection, and this branch is
# the one that most needs it: it is #3392's own guard, the escalation whose
# cause is hardest to work out from the outside (the answer is "a path nobody
# has classified", which the selection cannot express), and the only action it
# asks for is to go and classify the path it names. Deleting this echo used to
# leave the whole file green while the word "unclassified" appeared in seven
# comments here, so the branch read as covered without being pinned.
if ! grep -q "select-e2e:.*unclassified path.*thing\.conf" "$tmp/err"; then
echo "an unclassified path must be named on stderr; stderr was:" >&2
cat "$tmp/err" >&2
exit 1
fi
rm -rf "$tmp"
}
@test "inert repo meta selects nothing" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
printf '%s\n' .gitignore LICENSE .pre-commit-config.yaml > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ -z "$output" ]
rm -rf "$tmp"
}
@test "a non-e2e workflow selects nothing, an e2e one still escalates" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
# .github/ is inert as a directory, but the escalation for the workflows
# that run the suite is checked first and must win.
echo ".github/workflows/tags.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ -z "$output" ]
echo ".github/workflows/e2e-tag.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_full_suite "$output"
rm -rf "$tmp"
}
@test "a README inside an escalating tree stays inert" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
# packages/core/ escalates, but *.md is matched before that so a doc edit
# under it does not burn a full run.
echo "packages/core/installer/README.md" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ -z "$output" ]
rm -rf "$tmp"
}
@test "an inert path alongside a real one does not mask the selection" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
printf '%s\n' .gitignore packages/apps/postgres/values.yaml > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "postgres" ]
rm -rf "$tmp"
}
# The classification above is only total if every line reaches the loop that
# applies it. POSIX read assigns the last line and returns non-zero when the
# input has no trailing newline, so a plain `while read` drops it, and the
# fall-through that escalates an unrecognised path lives inside the body that
# drop skips. `git diff --name-only` always terminates its output, so the bug is
# invisible to a caller that pipes it; a caller assembling the list itself sees
# it immediately. Both cases below are red without `|| [ -n "$file" ]`.
#
# Cleanup here, as everywhere in this file, is the last statement of the body
# rather than a `trap ... EXIT`: that trap replaces the one the bats binary
# installs for its own bookkeeping, and a test failing under it can print no TAP
# line at all, which is the opposite of what a regression pin is for. Last, not
# before the assertion, so that `set -e` leaves the scratch directory behind on
# failure for inspection (docs/agents/e2e-testing.md §3).
@test "editing a disabled chainsaw suite selects nothing" {
# A .disabled suite runs nowhere, so its edits are the one file class that
# provably cannot regress a test — and before this rule they bought the
# most expensive outcome the selector has. The suffix was ignored when
# deriving the suite name, the derived name matched no existing suite, the
# final intersection came back empty, and the empty-selection safety net
# escalated to the full run.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "hack/e2e-chainsaw/kubernetes-proxmox/chainsaw-test.yaml.disabled" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ -z "$output" ]
rm -rf "$tmp"
}
@test "a disabled suite alongside a live one does not mask the selection" {
# The inert rule must drop the disabled path only, not the whole diff: the
# cheap way to make the test above pass is a rule that returns early, and
# that would silently unselect real work committed in the same change.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
printf '%s\n' hack/e2e-chainsaw/kubernetes-proxmox/chainsaw-test.yaml.disabled \
packages/apps/postgres/values.yaml > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "postgres" ]
rm -rf "$tmp"
}
@test "a top-level unit bats file selects nothing" {
# The non-e2e hack/*.bats files used to escalate to the full suite. The
# root Makefile is the authority on which of them the e2e sandbox runs:
# BATS_UNIT_FILES := $(filter-out hack/e2e-%.bats,$(wildcard hack/*.bats))
# keeps exactly these for the unit lane, and packages/core/testing's recipes
# execute only the ones it filters out. So the sandbox never runs one of
# these, and no Chainsaw suite can regress from an edit to one.
#
# Not a green gate with nothing behind it: `make unit-tests` DOES run them,
# gated on pull-requests.yaml's `code` output, which is computed there as
# "any path outside docs/" and never from this script.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
# Premise, read off the Makefile rather than assumed: the unit set is the
# hack/*.bats files whose names do not start with e2e-, and the file under
# test has to be in it.
grep -Fq 'BATS_UNIT_FILES := $(filter-out hack/e2e-%.bats,$(wildcard hack/*.bats))' Makefile
printf '%s\n' hack/select-e2e_test.bats hack/md-no-hardwrap.bats > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ -z "$output" ]
# The e2e-prefixed ones are the sandbox's own harness and must still
# escalate; the narrowing lives one alternative away from them in
# full_suite_pattern, so pin both sides of the prefix.
for f in hack/e2e-install-cozystack.bats hack/e2e-prepare-cluster.bats hack/e2e-test-openapi.bats; do
echo "$f" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources" 2>/dev/null)
assert_full_suite "$output"
done
# And an inert unit bats file beside a real app path must not mask it.
printf '%s\n' hack/select-e2e_test.bats packages/apps/postgres/values.yaml > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "postgres" ]
rm -rf "$tmp"
}
@test "the shared unit Bats helper selects nothing" {
# test_helper.bash is loaded only by BATS_UNIT_FILES. Enumerate this exact
# helper as inert rather than matching hack/*.bash: a future production
# helper must still hit the unclassified fail-safe until someone decides
# which e2e lane exercises it.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "hack/test_helper.bash" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ -z "$output" ]
# Inert means this path contributes nothing; it must not mask a real app
# path beside it.
printf '%s\n' hack/test_helper.bash packages/apps/postgres/values.yaml > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "postgres" ]
rm -rf "$tmp"
}
@test "every hack/*.bats file lands on the lane its name says" {
# The rule above is a claim about every hack/*.bats, asserted on two. This
# pins the claim itself: for every hack/*.bats in the tree, the selector's
# verdict must agree with the Makefile's split -- e2e-prefixed escalates,
# everything else selects nothing. A file added with a name that fits neither
# lane's expectation shows up here rather than on the next PR that touches it.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
full=$(full_suite_list)
for f in hack/*.bats; do
echo "$f" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources" 2>/dev/null)
case "$f" in
hack/e2e-*) want="$full" ;;
*) want="" ;;
esac
assert_selection "wrong lane for $f" "$output" "$want"
done
rm -rf "$tmp"
}
@test "a per-app e2e bats file selects that app's suite" {
# hack/e2e-apps/<name>.bats is the pre-Chainsaw per-app BATS suite, named
# after the app exactly as a Chainsaw suite directory is. Those paths matched
# no rule at all and escalated as unclassified -- 5 of the last 150 merged
# pull requests, all of them the migration deleting one of these files.
#
# Mapped rather than marked inert: the files formerly kept here were wired to
# nothing after migration, and inert would go quietly wrong if this layout
# returns. This fixture asserts the rule independently of the historical file
# inventory.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "hack/e2e-apps/postgres.bats" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "postgres" ]
# Alongside a real app path it must union, not replace -- the cheap way to
# pass the line above is a rule that returns early.
printf '%s\n' hack/e2e-apps/postgres.bats packages/apps/redis/values.yaml > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "postgres redis" ]
rm -rf "$tmp"
}
@test "a per-app e2e bats file naming no suite escalates on its own account" {
# The other half of the mapping, and the half that has to be decided HERE
# rather than by the final intersection. Deferring it made the verdict depend
# on the rest of the diff: alone the selection emptied and the backstop
# escalated, but beside any path that contributed a suite the escalation
# disappeared and the run narrowed to that suite with nothing on stderr --
# the merge-before-escalate shape #3330 removed from the graph walk.
#
# So both diffs are asserted, and the mixed one is the regression pin: the
# isolated case passes with the escalation deferred and cannot see the bug.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
# Premise: the name must really be absent from the suite list, or this
# measures the matched branch.
if full_suite_list | tr ' ' '\n' | grep -Fxq no-such-app; then
echo "premise broken: no-such-app is a real suite" >&2
exit 1
fi
echo "hack/e2e-apps/no-such-app.bats" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources" 2>"$tmp/err")
assert_full_suite "$output"
if ! grep -q "select-e2e:.*no-such-app.*names no runnable suite" "$tmp/err"; then
echo "an unmatched e2e-apps basename must name itself; stderr was:" >&2
cat "$tmp/err" >&2
exit 1
fi
printf '%s\n' hack/e2e-apps/no-such-app.bats packages/apps/redis/values.yaml > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources" 2>"$tmp/err")
assert_selection "an unmatched per-app bats file was swallowed by another path's suite" \
"$output" "$(full_suite_list)"
if ! grep -q "select-e2e:.*no-such-app.*names no runnable suite" "$tmp/err"; then
echo "the escalation must still be named in a mixed diff; stderr was:" >&2
cat "$tmp/err" >&2
exit 1
fi
rm -rf "$tmp"
}
@test "a nested path under e2e-apps escalates instead of being read as a suite" {
# POSIX case matches `/` with `*`, so the hack/e2e-apps/ arm sees nested paths
# too. With an unanchored capture the sed turned
# hack/e2e-apps/fixtures/postgres.bats into the "suite" fixtures/postgres --
# a name no suite carries, so the outcome was right by accident while the
# reason line printed a path where a suite name belongs.
#
# Nothing nested exists there today, which is exactly why the rule has to
# state what it does with one instead of letting it fall out of a regex.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
[ ! -d hack/e2e-apps/fixtures ]
for f in hack/e2e-apps/fixtures/postgres.bats hack/e2e-apps/helpers.sh; do
echo "$f" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources" 2>"$tmp/err")
assert_full_suite "$output"
if ! grep -q "select-e2e:.*shared material" "$tmp/err"; then
echo "a non-per-app path under e2e-apps must escalate as shared material; stderr was:" >&2
cat "$tmp/err" >&2
exit 1
fi
# The capture must never leak a path fragment into the suite name.
if grep -q "fixtures/postgres'" "$tmp/err"; then
echo "the reason line named a path where a suite name belongs; stderr was:" >&2
cat "$tmp/err" >&2
exit 1
fi
done
rm -rf "$tmp"
}
@test "helm-unittest fixtures and the enumerated gitattributes select nothing" {
# packages/tests/ is a helm-unittest fixture chart: changing a test OF
# cozy-lib does not change cozy-lib, no PackageSource lists these paths as a
# component, and nothing installs them. The .gitattributes named here hold
# nothing but linguist-generated markers, which reach no build, chart or test.
# Both classes fell through as unclassified and bought a full run -- 6 of the
# last 150 merged pull requests between them.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
printf '%s\n' packages/tests/cozy-lib-tests/tests/quota_test.yaml \
packages/tests/cozy-lib-tests/templates/tests/quota.yaml \
packages/system/.gitattributes \
packages/system/backup-controller/definitions/.gitattributes \
packages/system/backupstrategy-controller/definitions/.gitattributes > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ -z "$output" ]
# The .gitattributes rule is an ENUMERATION and must stay one. The
# justification is what those files contain, and the filename does not carry
# it -- .gitattributes can also set filter, eol, working-tree-encoding and
# export-subst, each of which changes what lands in the working tree and so
# what gets built. A by-name rule would make such a file inert in silence, so
# pin that a path not on the list is still classified by its own rule: at the
# repo root that is the unclassified fall-through.
echo ".gitattributes" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources" 2>/dev/null)
assert_full_suite "$output"
# The library itself is a different question and must still escalate: these
# fixtures test cozy-lib, so an inert rule reaching the library would be the
# dangerous over-reach here.
echo "packages/library/cozy-lib/templates/_quota.tpl" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources" 2>/dev/null)
assert_full_suite "$output"
# And an inert fixture beside a real app path must not mask the selection.
printf '%s\n' packages/tests/cozy-lib-tests/tests/quota_test.yaml \
packages/apps/postgres/values.yaml > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "postgres" ]
rm -rf "$tmp"
}
@test "a broken yq is named rather than passed off as an empty graph" {
# Both indexes are one yq each, read as `$(build_owners_index | sort -u)`,
# so the pipeline reports sort's status and set -e never sees yq's. A
# missing binary or a malformed PackageSource then yields an empty index,
# every path falls through to escalation, and the run is a full suite that
# looks exactly like intended conservatism. Nothing distinguishes it from
# "no owners matched", so nobody is told the selector stopped working.
#
# The escalation is the right outcome and is asserted here too; what this
# pins is that it comes with a line naming yq.
#
# The exit status is asserted as ZERO on purpose, and it is the difference
# between this case and the two below it. What a broken yq costs is the
# dependency graph, and the full suite is a correct answer without one, so
# the script still answers. When the SUITE LIST is what broke there is no
# correct answer left to give and the script exits non-zero instead. Both
# halves are pinned so the header and docs cannot drift into claiming this
# one reddens the gate — it does not; it is conservative and quiet.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
mkdir "$tmp/bin"
printf '#!/bin/sh\necho "yq: broken fixture" >&2\nexit 1\n' > "$tmp/bin/yq"
chmod +x "$tmp/bin/yq"
echo "packages/apps/postgres/values.yaml" > "$tmp/diff"
rc=0
output=$(PATH="$tmp/bin:$PATH" hack/select-e2e.sh "$tmp/diff" "$tmp/sources" 2>"$tmp/err") || rc=$?
if [ "$rc" -ne 0 ]; then
echo "a broken yq must still answer with the full suite, not exit $rc" >&2
cat "$tmp/err" >&2
exit 1
fi
# Matched on the script's own prefix, not on the word alone: the stub writes
# to stderr the way a real yq does, so a bare `grep yq` passes on the
# fixture's noise and pins nothing.
if ! grep -q 'select-e2e:.*yq' "$tmp/err"; then
echo "a yq failure must be reported by select-e2e itself; stderr was:" >&2
cat "$tmp/err" >&2
exit 1
fi
assert_full_suite "$output"
rm -rf "$tmp"
}
@test "a broken find is reported rather than yielding an empty suite list" {
# The suite list is built as `$(find ... | sed | sort)`, which reports
# sort's status and never find's — the same blindness as the yq indexes
# above, in a worse place: every escalation prints that list, so a silent
# failure here turns "run everything" into a run of nothing.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
mkdir "$tmp/bin"
printf '#!/bin/sh\necho "find: broken fixture" >&2\nexit 1\n' > "$tmp/bin/find"
chmod +x "$tmp/bin/find"
echo "go.mod" > "$tmp/diff"
rc=0
output=$(PATH="$tmp/bin:$PATH" hack/select-e2e.sh "$tmp/diff" "$tmp/sources" 2>"$tmp/err") || rc=$?
if [ "$rc" -eq 0 ]; then
echo "a broken find must fail the run; it exited 0 with output: '$output'" >&2
exit 1
fi
if ! grep -q 'select-e2e:.*find' "$tmp/err"; then
echo "a find failure must be reported by select-e2e itself; stderr was:" >&2
cat "$tmp/err" >&2
exit 1
fi
rm -rf "$tmp"
}
@test "an empty suite list is fatal whatever the diff classifies as" {
# Checked once where the list is built, not at the escalation branches,
# because an empty list corrupts more than those. The escalations would
# print it, and a blank selection is what both lanes read as "skip
# Chainsaw" before posting the required status green — the outcome meaning
# "run everything" delivering a run of nothing. But the
# examples/backups/<app>/ rule escalates nothing and still consults the
# list as a membership test, so with the list empty a backup-harness edit
# that should run its suite reports nothing to run instead — and that one
# is indistinguishable from a legitimately empty selection.
#
# Run from a tree whose hack/e2e-chainsaw exists but holds no
# chainsaw-test.yaml, so find succeeds and matches nothing — the state a
# moved directory or a wrong working directory produces. All three diff
# classes are exercised: one that escalates, one that selects by
# membership, and one that legitimately selects nothing. The middle two
# exited 0 while the guard lived at the escalation branches.
tmp=$(mktemp -d)
script="$PWD/hack/select-e2e.sh"
cp -r packages/core/platform/sources "$tmp/sources"
mkdir -p "$tmp/tree/hack/e2e-chainsaw"
for d in go.mod examples/backups/postgres/run-all.sh docs/x.md; do
echo "$d" > "$tmp/diff"
rc=0
output=$(cd "$tmp/tree" && "$script" "$tmp/diff" "$tmp/sources" 2>"$tmp/err") || rc=$?
if [ "$rc" -eq 0 ]; then
echo "a broken suite enumeration must fail for '$d', not print '$output' and exit 0" >&2
cat "$tmp/err" >&2
exit 1
fi
if ! grep -q 'suite enumeration is broken' "$tmp/err"; then
echo "the refusal must say why for '$d'; stderr was:" >&2
cat "$tmp/err" >&2
exit 1
fi
done
rm -rf "$tmp"
}
@test "a suite written as chainsaw-test.yml is discovered" {
# Chainsaw loads chainsaw-test.yml when there is no chainsaw-test.yaml, so a
# suite spelled that way runs. Enumerated by the .yaml name alone it was
# absent from every escalation and unselectable by its own per-suite edits,
# and a suite that exists but is never selected reds nothing.
tmp=$(mktemp -d)
script="$PWD/hack/select-e2e.sh"
cp -r packages/core/platform/sources "$tmp/sources"
mkdir -p "$tmp/tree/hack/e2e-chainsaw/alpha" "$tmp/tree/hack/e2e-chainsaw/beta"
: > "$tmp/tree/hack/e2e-chainsaw/alpha/chainsaw-test.yaml"
: > "$tmp/tree/hack/e2e-chainsaw/beta/chainsaw-test.yml"
# A directory holding both spellings is still one suite.
: > "$tmp/tree/hack/e2e-chainsaw/alpha/chainsaw-test.yml"
echo go.mod > "$tmp/diff"
output=$(cd "$tmp/tree" && "$script" "$tmp/diff" "$tmp/sources" 2>/dev/null)
assert_selection "the full suite must include a .yml suite" "$output" "alpha beta"
echo hack/e2e-chainsaw/beta/step.yaml > "$tmp/diff"
output=$(cd "$tmp/tree" && "$script" "$tmp/diff" "$tmp/sources")
assert_selection "a per-suite edit must select a .yml suite" "$output" "beta"
rm -rf "$tmp"
}
@test "a suite made only of numbered step files is discovered" {
# With no chainsaw-test.* in a directory, Chainsaw builds a Test from files
# named like 01-install.yaml (TryFindStepFiles), so that directory runs as a
# suite whenever the whole tree is handed to chainsaw. Listed by the
# chainsaw-test name alone, it was never selected.
tmp=$(mktemp -d)
script="$PWD/hack/select-e2e.sh"
cp -r packages/core/platform/sources "$tmp/sources"
mkdir -p "$tmp/tree/hack/e2e-chainsaw/alpha" "$tmp/tree/hack/e2e-chainsaw/gamma" \
"$tmp/tree/hack/e2e-chainsaw/delta"
: > "$tmp/tree/hack/e2e-chainsaw/alpha/chainsaw-test.yaml"
: > "$tmp/tree/hack/e2e-chainsaw/gamma/01-install.yaml"
: > "$tmp/tree/hack/e2e-chainsaw/gamma/01-assert.yml"
# Not a step file: the number has to lead the name.
: > "$tmp/tree/hack/e2e-chainsaw/delta/install-01.yaml"
# Nor is a name with no number at all.
: > "$tmp/tree/hack/e2e-chainsaw/delta/-install.yaml"
# Not a step file either: Chainsaw skips directories. It keeps a symlink,
# though, because a symlink's directory entry is not a directory.
mkdir -p "$tmp/tree/hack/e2e-chainsaw/theta/01-x.yaml" "$tmp/tree/hack/e2e-chainsaw/iota" \
"$tmp/tree/hack/e2e-chainsaw/kappa"
ln -s ../alpha/chainsaw-test.yaml "$tmp/tree/hack/e2e-chainsaw/iota/01-link.yaml"
# Step files spelled .yml alone make a suite too.
: > "$tmp/tree/hack/e2e-chainsaw/kappa/01-install.yml"
# A directory named chainsaw-test.yaml is still read by Chainsaw, which
# then fails loudly, so its suite stays listed.
mkdir -p "$tmp/tree/hack/e2e-chainsaw/lambda/chainsaw-test.yaml" \
"$tmp/tree/hack/e2e-chainsaw/mu/chainsaw-test.yml"
echo go.mod > "$tmp/diff"
output=$(cd "$tmp/tree" && "$script" "$tmp/diff" "$tmp/sources" 2>/dev/null)
assert_selection "the full suite must include a step-file suite" "$output" "alpha gamma iota kappa lambda mu"
echo hack/e2e-chainsaw/gamma/01-install.yaml > "$tmp/diff"
output=$(cd "$tmp/tree" && "$script" "$tmp/diff" "$tmp/sources")
assert_selection "a per-suite edit must select a step-file suite" "$output" "gamma"
rm -rf "$tmp"
}
@test "a suite nested below a top-level directory is discovered" {
# Chainsaw walks every directory under the path it is given, so a test in
# hack/e2e-chainsaw/<dir>/<sub>/ runs whenever <dir> is handed to chainsaw,
# with or without a test of its own at the top. Read off the top level
# alone, <dir> was never listed, so even the full suite left it out.
tmp=$(mktemp -d)
script="$PWD/hack/select-e2e.sh"
cp -r packages/core/platform/sources "$tmp/sources"
mkdir -p "$tmp/tree/hack/e2e-chainsaw/alpha" "$tmp/tree/hack/e2e-chainsaw/epsilon/cases" \
"$tmp/tree/hack/e2e-chainsaw/eta/cases/deep"
: > "$tmp/tree/hack/e2e-chainsaw/alpha/chainsaw-test.yaml"
: > "$tmp/tree/hack/e2e-chainsaw/epsilon/cases/chainsaw-test.yml"
: > "$tmp/tree/hack/e2e-chainsaw/eta/cases/deep/02-apply.yaml"
echo go.mod > "$tmp/diff"
output=$(cd "$tmp/tree" && "$script" "$tmp/diff" "$tmp/sources" 2>/dev/null)
assert_selection "the full suite must include a nested suite" "$output" "alpha epsilon eta"
echo hack/e2e-chainsaw/eta/cases/deep/02-apply.yaml > "$tmp/diff"
output=$(cd "$tmp/tree" && "$script" "$tmp/diff" "$tmp/sources")
assert_selection "an edit to a nested suite must select its top-level directory" "$output" "eta"
rm -rf "$tmp"
}
@test "an unterminated last line is still classified" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
printf '%s' packages/apps/postgres/values.yaml > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "postgres" ]
rm -rf "$tmp"
}
@test "an unterminated unclassified last line still escalates" {
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
# The unclassified path is the one the drop would eat, so without the guard
# the escalation never fires and the selection comes back empty — the exact
# silent-green this classification exists to remove.
printf 'docs/x.md\n%s' brand-new/thing.conf > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_full_suite "$output"
rm -rf "$tmp"
}
# Coverage is decided per changed path, and the tests below pin both directions
# of that: a path reaching no suite escalates whatever else the diff selected,
# and a path that is covered never drags the full suite in with it.
@test "a package covered by no suite escalates despite other selections" {
# cozystack-basics is in the graph and reaches no runnable Chainsaw suite,
# so a change to it must run everything. That escalation belongs to the
# changed path, not to the shape of the rest of the diff: adding an
# unrelated per-suite Chainsaw edit — the ordinary "change a platform
# component and adjust the e2e next to it" PR — used to narrow the run down
# to that one suite (#3330).
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
# Guard the premise: if the package ever leaves the graph it escalates as an
# unrecognised packages/ path instead, and this test passes on the wrong
# rule.
grep -rq 'path: system/cozystack-basics' "$tmp/sources"
echo "packages/system/cozystack-basics/templates/ingress-hostname-policy.yaml" > "$tmp/diff"
alone=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources" 2>"$tmp/err")
assert_full_suite "$alone"
# The escalation has to say it happened. This is the branch most packages
# take, so a full run with nothing on stderr leaves "why did everything
# run" unanswerable — the same complaint this change makes about the old
# swallowed escalation. Matched on the script's own prefix and on the source
# it names, not on the word "escalating" alone, which the unclassified
# fall-through also prints.
if ! grep -q 'select-e2e:.*cozystack\.cozystack-basics' "$tmp/err"; then
echo "the per-path escalation must name the sources it escalated for; stderr was:" >&2
cat "$tmp/err" >&2
exit 1
fi
echo "hack/e2e-chainsaw/postgres/chainsaw-test.yaml" >> "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_selection "a per-suite edit swallowed the full-suite escalation" \
"$output" "$(full_suite_list)"
rm -rf "$tmp"
}
@test "escalation also survives another package that does select suites" {
# The swallowing was not specific to Chainsaw edits: any other changed path
# contributing a suite name hid it, a second packages/ path included.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
printf '%s\n' packages/system/cozystack-basics/templates/ingress-hostname-policy.yaml \
packages/apps/postgres/values.yaml > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_full_suite "$output"
rm -rf "$tmp"
}
@test "two covered packages still narrow to their own suites" {
# The per-path loop is the code that can over-escalate, so pin the negative
# direction too: a path that IS covered must never pull the full suite in.
# Without this the cheapest way to pass every test above is to escalate
# always.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
printf '%s\n' packages/apps/postgres/values.yaml packages/apps/redis/values.yaml > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "postgres redis" ]
rm -rf "$tmp"
}
@test "a path owned by several sources counts as covered if any one is" {
# system/postgres-operator belongs to two PackageSources: cozystack.monitoring
# and cozystack.postgres-operator, which reaches postgres-application,
# harbor-application (Harbor uses postgres as its backing DB),
# monitoring-application, and keycloak, which maps to the suites driving
# Keycloak clients. Coverage is decided over the path's sources
# together, which is why the unit is the path and not the source — deciding
# per source would escalate on a half that reaches nothing and run everything
# for every change here. cozystack.monitoring maps to the monitoring
# suite by name, so it is renamed below to a source that reaches no suite,
# which is the half this test needs.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
yq -i '.metadata.name = "cozystack.monitoring-stack"' "$tmp/sources/monitoring.yaml"
# Guard the premise. Filter outside yq: a trailing `| $n` re-emits the
# binding whether or not the select() matched, so counting that way returns
# every source in the graph and can never fail. Route the yq output through
# `echo "$VAR" | awk`, the same as select-e2e.sh's own owners split, because
# mikefarah yq emits `"\t"` in a concatenation as a literal backslash-t and
# `echo` is what turns it into the tab `awk -F'\t'` splits on.
owners_tsv=$(yq -rN '.metadata.name as $n | .spec.variants[]?.components[]?.path | select(. != null) | . + "\t" + $n' "$tmp/sources"/*.yaml)
owners=$(echo "$owners_tsv" | awk -F'\t' '$1=="system/postgres-operator"{print $2}' | sort -u | wc -l)
[ "$owners" -ge 2 ]
echo "packages/system/postgres-operator/values.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "harbor kubernetes-latest monitoring postgres" ]
rm -rf "$tmp"
}
@test "a suite owned by a non-application source is reachable from its package" {
# kuberture and securitygroup stand on sources named neither *-application
# nor after the suite, and external-dns on one that carries the suite name
# itself. All three map through src_to_suites, so a change to their package
# selects that suite instead of escalating the whole run (#3665).
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "packages/system/kuberture/values.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "kuberture" ]
echo "packages/system/securitygroup-controller/values.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "securitygroup" ]
echo "packages/system/external-dns/values.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
[ "$output" = "external-dns" ]
rm -rf "$tmp"
}
@test "an ingress-nginx change selects the gateway admission regression" {
# system/ingress-nginx is shared by the root ingress package, the tenant
# ingress application, and tenant Kubernetes clusters. The gateway suite
# owns the host-cluster Ingress admission regression, while the two
# Kubernetes suites cover the copies installed inside tenant clusters --
# two, not four, since the OIDC pair was folded into kubernetes-latest --
# and computeplane covers the copy its module pins inside its cluster.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "packages/system/ingress-nginx/templates/admission-webhook-egress-policy.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_selection "an ingress-nginx change must exercise every installed copy" \
"$output" "computeplane gateway kubernetes-latest kubernetes-previous"
rm -rf "$tmp"
}
@test "a kubernetes-nodes pool chart change selects computeplane and both kubernetes suites" {
# apps/kubernetes-nodes belongs to kubernetes-nodes-application and to
# computeplane-application. The kubernetes suites create KubernetesNodes
# natively and computeplane renders pools through its module, so a pool
# chart change has to reach all three, not only the source that happens
# to have a suite of its own name.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "packages/apps/kubernetes-nodes/values.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_selection "a pool chart change must exercise native and module pools" \
"$output" "computeplane kubernetes-latest kubernetes-previous"
rm -rf "$tmp"
}
@test "a kubernetes-nodes-rd change selects computeplane and both kubernetes suites" {
# kubernetes-nodes-rd ships only in kubernetes-nodes-application, which
# maps to the same three suites as the pool chart it registers.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
echo "packages/system/kubernetes-nodes-rd/cozyrds/kubernetes-nodes.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_selection "a kubernetes-nodes-rd change must exercise native and module pools" \
"$output" "computeplane kubernetes-latest kubernetes-previous"
rm -rf "$tmp"
}
@test "a keycloak change selects both suites that drive Keycloak clients" {
# monitoring and kubernetes-latest both create a KeycloakClient and assert
# it is removed again, which goes through the operator's finalizer. Only
# monitoring-application declares the keycloak-operator edge, so the graph
# alone reaches monitoring and drops the tenant Kubernetes OIDC lifecycle.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
for path in packages/system/keycloak/values.yaml packages/system/keycloak-operator/values.yaml; do
echo "$path" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_selection "a change to $path must run every Keycloak client lifecycle" \
"$output" "kubernetes-latest monitoring"
done
rm -rf "$tmp"
}
@test "an etcd-operator change selects the etcd suite, not the whole run" {
# cozystack.etcd-operator reached no runnable suite, so every change to the
# operator ran all 21 -- 5 of the last 150 merged pull requests. The suite
# exists and the app genuinely needs the operator (extra/etcd renders kind:
# EtcdCluster from etcd-operator.cozystack.io/v1alpha2), so what was missing
# was the dependsOn edge that makes the app reachable from it. Both operator
# components are asserted: the reverse walk starts from whichever
# PackageSource owns the changed path, and the CRDs sit in the same source.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
# Guard the premise: without the edge this passes for the wrong reason,
# because the full suite contains "etcd" too.
grep -q 'cozystack\.etcd-operator' "$tmp/sources/etcd-application.yaml"
for path in packages/system/etcd-operator/values.yaml \
packages/system/etcd-operator-crds/templates/etcd-clusters.yaml; do
echo "$path" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_selection "an etcd-operator change must select only the etcd suite" \
"$output" "etcd"
done
rm -rf "$tmp"
}
@test "a seaweedfs change selects every suite that uses seaweedfs object storage" {
# cozystack.seaweedfs-application used to map to a seaweedfs suite that did
# not exist, so every seaweedfs change ran the whole suite. The seaweedfs
# suite, which runs an instance of its own, is not the only one it can
# break. bucket drives tenant-root's instance, the backup round-trips write
# to that instance through the in-cluster seaweedfs-s3 endpoint, most of
# them verifying its CA, which bucket skips; harbor gets its registry
# storage from a BucketClaim served by the seaweedfs COSI driver, and no
# graph edge links the two. The expected set is read off the suites' own
# yaml rather than written here, so a new suite that names the endpoint or
# a BucketClaim there and is left out of src_to_suites goes red. A suite reaching seaweedfs only through a
# sourced script is invisible to the scan; the round-trips pass S3_ENDPOINT
# in their yaml, which is what makes it hold today. Comment lines are
# skipped, which is what keeps etcd out: its round-trip is gated off in CI
# and names the endpoint only in prose.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
want=''
for s in $(full_suite_list); do
if cat hack/e2e-chainsaw/"$s"/*.y*ml | grep -v '^[[:space:]]*#' \
| grep -qE 'seaweedfs-s3|BucketClaim|objectstorage\.k8s\.io'; then
want="${want:+$want }$s"
fi
done
# Premise: the scan must find the suite that exists for this, or it is
# reading the wrong files.
case " $want " in *" bucket "*) ;; *) echo "premise broken: scan found '$want'" >&2; exit 1 ;; esac
for path in packages/system/seaweedfs/values.yaml packages/extra/seaweedfs/values.yaml \
packages/system/seaweedfs-db/values.yaml packages/system/seaweedfs-rd/Chart.yaml; do
echo "$path" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources")
assert_selection "a seaweedfs change must select the suites that use it: $path" \
"$output" "$want"
done
rm -rf "$tmp"
}
@test "every suite owner round-trips through selection and install closure" {
# select-install.sh maps a suite to the PackageSource that installs it, and
# select-e2e.sh must map that source back to the suite. A suite that does
# not round-trip is unreachable from its own package. Exercise the actual
# scripts as well as the two mapping functions so a full-suite fallback or
# a closure that happens to contain a baseline source cannot pass vacuously.
eval "$(sed -n '/^src_to_suites()/,/^}/p' hack/select-e2e.sh)"
eval "$(sed -n '/^suite_to_source()/,/^}/p' hack/select-install.sh)"
# suite_to_source falls back to probing the graph for a source of that name,
# and reads it from NODES, which its own script builds before calling it.
NODES=$(yq -rN '.metadata.name' packages/core/platform/sources/*.yaml | sort -u)
tmp=$(mktemp -d)
for suite in $(full_suite_list | tr ' ' '\n'); do
owners=$(suite_to_source "$suite")
if [ -z "$owners" ]; then
echo "suite '$suite' has no source in select-install.sh's suite_to_source" >&2
exit 1
fi
for src in $owners; do
if ! src_to_suites "${src#cozystack.}" | tr ' ' '\n' | grep -Fxq "$suite"; then
echo "suite '$suite' maps to $src, which select-e2e.sh maps back to '$(src_to_suites "${src#cozystack.}")'" >&2
exit 1
fi
source_file="packages/core/platform/sources/${src#cozystack.}.yaml"
component_path=$(yq -r '.spec.variants[].components[]?.path | select(. != null)' "$source_file" | sed -n '1p')
component_path="packages/$component_path"
files=$(find "$component_path" -type f ! -name '*.md')
changed=$(printf '%s\n' "$files" | sort | sed -n '1p')
if [ -z "$changed" ]; then
echo "suite owner '$src' has no real component file for the selector matrix" >&2
exit 1
fi
printf '%s\n' "$changed" > "$tmp/diff"
selected=$(hack/select-e2e.sh "$tmp/diff")
if [ "$selected" = "$(full_suite_list)" ]; then
echo "owner '$src' reached '$suite' only through full-suite fallback for $changed" >&2
exit 1
fi
if ! printf '%s\n' "$selected" | tr ' ' '\n' | grep -Fxq "$suite"; then
echo "owner '$src' did not select its suite '$suite' for $changed: $selected" >&2
exit 1
fi
closure=$(hack/select-install.sh "$selected")
if ! printf '%s\n' "$closure" | tr ' ' '\n' | grep -Fxq "$src"; then
echo "selection '$selected' does not install owner '$src' for suite '$suite'" >&2
exit 1
fi
done
done
rm -rf "$tmp"
}
@test "an edit to a non-suite directory under e2e-chainsaw escalates on its own account" {
# Only a switched-off suite is ignorable. Shared material next to _lib/ is
# invisible to all_apps, and selecting nothing for it would skip E2E
# outright.
#
# The mixed diff is the regression pin. When the name was left for the final
# intersection to drop, the path escalated alone, through the empty-selection
# backstop, but beside any path that contributed a suite the run narrowed to
# that suite with nothing on stderr -- the merge-before-escalate shape #3330
# removed from the graph walk. The single-path case passes either way.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
# Premise: the directory must not exist, or all_apps would hold it and the
# test would be measuring the ordinary per-suite rule.
[ ! -d hack/e2e-chainsaw/_fixtures ]
for diff in "hack/e2e-chainsaw/_fixtures/tenant.yaml" \
"hack/e2e-chainsaw/_fixtures/tenant.yaml packages/apps/postgres/values.yaml" \
"hack/e2e-chainsaw/postgres/chainsaw-test.yaml hack/e2e-chainsaw/_fixtures/tenant.yaml"; do
printf '%s\n' $diff > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources" 2>"$tmp/err")
assert_selection "a non-suite directory under e2e-chainsaw was not escalated for: $diff" \
"$output" "$(full_suite_list)"
if ! grep -q "select-e2e: 'hack/e2e-chainsaw/_fixtures/tenant.yaml' names no runnable suite ('_fixtures')" "$tmp/err"; then
echo "the escalation must name the path and the directory it read; stderr was:" >&2
cat "$tmp/err" >&2
exit 1
fi
done
rm -rf "$tmp"
}
@test "a packages/ path with no graph entry escalates and says which" {
# The fourth branch that used to escalate in silence: a path under
# packages/(apps|system|extra)/<name>/ that no PackageSource lists as a
# component path. That is how a new package looks before its source lands, so
# the line has to name the package dir rather than the rule — the reader's
# next step is to add it to the graph.
tmp=$(mktemp -d)
cp -r packages/core/platform/sources "$tmp/sources"
# Premise: no source may claim this path, or the graph lookup succeeds and
# the test measures the ordinary component rule instead.
if grep -rq 'path: system/zz-not-a-package' "$tmp/sources"; then
echo "premise broken: system/zz-not-a-package is in the graph" >&2
exit 1
fi
echo "packages/system/zz-not-a-package/values.yaml" > "$tmp/diff"
output=$(hack/select-e2e.sh "$tmp/diff" "$tmp/sources" 2>"$tmp/err")
assert_full_suite "$output"
if ! grep -q "select-e2e:.*system/zz-not-a-package.*component path" "$tmp/err"; then
echo "an unowned packages/ path must name itself; stderr was:" >&2
cat "$tmp/err" >&2
exit 1
fi
rm -rf "$tmp"
}
@test "resolve_suites still has exactly one call site" {
# resolve_suites writes its working variables into the caller's scope (POSIX
# sh, no local). That is safe only because its single call sits inside $( ).
# A second call added without noticing would clobber the loop state around
# it silently, so this guards the note above the function: in code the name
# belongs on its definition line and on that one call, nowhere else.
#
# Comment lines are dropped first, or the guard's own subject goes out of
# bounds — describing the hazard above the function would turn it red for
# prose. Occurrences rather than matching lines, so a second call folded
# onto the line the first sits on still moves the number.
count=$(grep -v '^[[:space:]]*#' hack/select-e2e.sh | grep -o 'resolve_suites' | wc -l)
[ "$count" -eq 2 ]
}