Files
cozystack/hack/select-install_test.bats
Aleksei Sviridkin 5fde704a30 fix(ci): skip directories named like Chainsaw step files
Chainsaw builds step files only from directory entries that are not
directories, so a directory named like 01-x.yaml makes nothing a
suite. Both selectors listed it as one, and select-install demanded a
mapping for it. A symlink still counts, as it does for Chainsaw. A
directory named chainsaw-test.yaml keeps its suite listed: Chainsaw
reads that name whatever its type and fails loudly on a directory, and
an unlisted suite would hide that failure.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-01 21:07:05 +03:00

337 lines
14 KiB
Bash

#!/usr/bin/env bats
# -----------------------------------------------------------------------------
# Unit tests for hack/select-install.sh
#
# cozytest.sh's awk parser recognizes only @test blocks and a bare `}` on its
# own line; there is no bats `run` or `$status`. Each test runs as a shell
# function under `set -eu -x`, so assertions are direct shell tests that exit
# non-zero on failure. setup()/teardown() are not honored, and cleanup goes on
# the last line of the body rather than in an EXIT trap -- a trap there replaces
# the bats binary's own, and a failing test then prints no TAP line at all. To
# assert a NON-zero exit, invert with `if` so `set -e` doesn't abort the test.
#
# Tests that only READ the production sources call the script directly (it
# defaults sources-dir to packages/core/platform/sources); only the tests that
# build a synthetic graph or suite tree use a scratch dir.
#
# Run with: hack/cozytest.sh hack/select-install_test.bats
# -----------------------------------------------------------------------------
@test "single app selects its forward dependency closure" {
output=$(hack/select-install.sh "postgres")
echo "$output" | grep -wq cozystack.postgres-application
echo "$output" | grep -wq cozystack.postgres-operator
echo "$output" | grep -wq cozystack.networking
# engine edge is KEPT on the install walk (unlike select-e2e.sh)
echo "$output" | grep -wq cozystack.cozystack-engine
}
@test "closure is transitive (deps of deps are pulled in)" {
output=$(hack/select-install.sh "postgres")
# cert-manager is a 2-hop dep (via postgres-operator and via the engine)
echo "$output" | grep -wq cozystack.cert-manager
# gateway-api-crds is a 3-hop dep (postgres-application -> networking -> gateway-api-crds)
echo "$output" | grep -wq cozystack.gateway-api-crds
}
@test "app pulls its direct operator dependencies" {
output=$(hack/select-install.sh "harbor")
echo "$output" | grep -wq cozystack.harbor-application
echo "$output" | grep -wq cozystack.postgres-operator
echo "$output" | grep -wq cozystack.redis-operator
echo "$output" | grep -wq cozystack.objectstorage-controller
}
@test "kubernetes suites map back to the kubernetes application source" {
output=$(hack/select-install.sh "kubernetes-latest")
echo "$output" | grep -wq cozystack.kubernetes-application
}
@test "multiple suites union their closures" {
output=$(hack/select-install.sh "postgres kafka")
echo "$output" | grep -wq cozystack.postgres-application
echo "$output" | grep -wq cozystack.kafka-application
}
@test "suite list can be read from stdin with -" {
output=$(printf '%s\n' "postgres kafka" | hack/select-install.sh -)
echo "$output" | grep -wq cozystack.postgres-application
echo "$output" | grep -wq cozystack.kafka-application
echo "$output" | grep -wq cozystack.cozystack-engine
}
@test "empty suites select nothing" {
output=$(hack/select-install.sh "")
[ -z "$output" ]
}
@test "unmapped suites are a hard error, all reported, no partial output" {
# Fail closed: a suite that maps to nothing must abort, not silently emit an
# empty set (which would install nothing and let the run pass vacuously).
# Also lock the contract that EVERY bad suite is reported in one run (not
# just the first) and that no partial closure leaks to stdout.
out=$(mktemp); err=$(mktemp)
if hack/select-install.sh "bad-one postgres bad-two" >"$out" 2>"$err"; then
echo "expected a non-zero exit for unmapped suites" >&2
exit 1
fi
grep -q "bad-one" "$err"
grep -q "bad-two" "$err"
[ ! -s "$out" ]
rm -f "$out" "$err"
}
@test "suite whose source omits the -application suffix resolves via fallback" {
# kuberture's PackageSource is cozystack.kuberture (no -application suffix)
output=$(hack/select-install.sh "kuberture")
echo "$output" | grep -wq cozystack.kuberture
}
@test "securitygroup closure includes the engine that serves sdn.cozystack.io" {
# Regression for the under-selection bug: the securitygroup suite applies
# sdn.cozystack.io/v1alpha1, served by the aggregated apiserver
# (a cozystack-engine component). The forward walk from the controller can't
# reach the engine, so it must be seeded. Assert the controller AND the
# engine AND the engine's stable prerequisites — asserting only the
# controller (a single member) is what let the bug through.
output=$(hack/select-install.sh "securitygroup")
echo "$output" | grep -wq cozystack.securitygroup-controller
echo "$output" | grep -wq cozystack.cozystack-engine
echo "$output" | grep -wq cozystack.cert-manager
echo "$output" | grep -wq cozystack.networking
echo "$output" | grep -wq cozystack.gateway-api-crds
}
@test "etcd closure includes the operator that serves its CRD" {
# The other half of the same missing edge. extra/etcd renders kind:
# EtcdCluster from etcd-operator.cozystack.io/v1alpha2, so an etcd suite
# installed without cozystack.etcd-operator has no CRD to apply against and
# fails on `no matches for kind "EtcdCluster"`. The forward walk reaches the
# operator only through etcd-application's dependsOn, so this asserts the
# operator AND the deps it drags in -- asserting only the app source is what
# left the gap invisible.
output=$(hack/select-install.sh "etcd")
echo "$output" | grep -wq cozystack.etcd-application
echo "$output" | grep -wq cozystack.etcd-operator
echo "$output" | grep -wq cozystack.cert-manager
echo "$output" | grep -wq cozystack.vertical-pod-autoscaler
echo "$output" | grep -wq cozystack.cozystack-engine
}
@test "validate passes on the real source graph and suite mapping" {
hack/select-install.sh --validate
}
@test "validate detects a dangling dependency" {
tmp=$(mktemp -d)
mkdir -p "$tmp/sources" "$tmp/suites"
cat > "$tmp/sources/foo-application.yaml" <<'YAML'
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.foo-application
spec:
variants:
- name: default
dependsOn:
- cozystack.does-not-exist
YAML
# empty suites-dir isolates this to the graph check
if hack/select-install.sh --validate "$tmp/sources" "$tmp/suites" 2>/dev/null; then
echo "expected validation to fail on a dangling dependency" >&2
exit 1
fi
rm -rf "$tmp"
}
@test "validate detects a dependency cycle" {
tmp=$(mktemp -d)
mkdir -p "$tmp/sources" "$tmp/suites"
cat > "$tmp/sources/a.yaml" <<'YAML'
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.a
spec:
variants:
- name: default
dependsOn:
- cozystack.b
YAML
cat > "$tmp/sources/b.yaml" <<'YAML'
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.b
spec:
variants:
- name: default
dependsOn:
- cozystack.a
YAML
# empty suites-dir isolates this to the graph check
if hack/select-install.sh --validate "$tmp/sources" "$tmp/suites" 2>/dev/null; then
echo "expected validation to fail on a dependency cycle" >&2
exit 1
fi
rm -rf "$tmp"
}
@test "validate detects a suite directory with no source mapping" {
tmp=$(mktemp -d)
mkdir -p "$tmp/suites/totally-unmapped-suite"
: > "$tmp/suites/totally-unmapped-suite/chainsaw-test.yaml"
# real graph (so graph checks pass); the only failure is the unmapped suite
if hack/select-install.sh --validate packages/core/platform/sources "$tmp/suites" 2>/dev/null; then
echo "expected validation to fail on an unmapped suite dir" >&2
exit 1
fi
rm -rf "$tmp"
}
@test "validate sees a suite written as chainsaw-test.yml" {
# Chainsaw runs a chainsaw-test.yml suite, so the mapping check has to see
# one too, or an unmapped suite spelled that way passes validation.
tmp=$(mktemp -d)
mkdir -p "$tmp/suites/totally-unmapped-suite"
: > "$tmp/suites/totally-unmapped-suite/chainsaw-test.yml"
err=$(hack/select-install.sh --validate packages/core/platform/sources "$tmp/suites" 2>&1 1>/dev/null) && {
echo "expected validation to fail on an unmapped .yml suite" >&2
exit 1
}
echo "$err" | grep -q "suite 'totally-unmapped-suite' has no PackageSource mapping"
rm -rf "$tmp"
}
@test "validate sees a suite made only of numbered step files" {
# Chainsaw runs a directory of 01-*.yaml step files as a suite when it holds
# no chainsaw-test.*, so the mapping check has to see that layout too. The
# step file is a symlink because Chainsaw counts one.
tmp=$(mktemp -d)
mkdir -p "$tmp/suites/totally-unmapped-suite"
: > "$tmp/install.yaml"
ln -s "$tmp/install.yaml" "$tmp/suites/totally-unmapped-suite/00-install.yaml"
err=$(hack/select-install.sh --validate packages/core/platform/sources "$tmp/suites" 2>&1 1>/dev/null) && {
echo "expected validation to fail on an unmapped step-file suite" >&2
exit 1
}
echo "$err" | grep -q "suite 'totally-unmapped-suite' has no PackageSource mapping"
rm -rf "$tmp"
}
@test "validate ignores what Chainsaw does not load as a step file" {
# Chainsaw skips a step-file name when the entry is a directory, and a name
# with no leading number is not a step file, so neither makes a suite that
# needs a mapping.
tmp=$(mktemp -d)
mkdir -p "$tmp/suites/postgres" "$tmp/suites/totally-unmapped-suite/01-x.yaml"
: > "$tmp/suites/totally-unmapped-suite/-install.yaml"
: > "$tmp/suites/postgres/chainsaw-test.yaml"
hack/select-install.sh --validate packages/core/platform/sources "$tmp/suites"
rm -rf "$tmp"
}
@test "validate sees a directory named chainsaw-test.yaml or .yml" {
# Chainsaw stats chainsaw-test.yaml or .yml without checking its type, so a
# directory of that name fails discovery loudly. Listing its suite keeps
# that failure where a run reaches it instead of leaving the suite out.
for name in chainsaw-test.yaml chainsaw-test.yml; do
tmp=$(mktemp -d)
mkdir -p "$tmp/suites/totally-unmapped-suite/$name"
: > "$tmp/suites/totally-unmapped-suite/$name/x"
err=$(hack/select-install.sh --validate packages/core/platform/sources "$tmp/suites" 2>&1 1>/dev/null) && {
echo "expected validation to fail on an unmapped suite with a $name directory" >&2
exit 1
}
echo "$err" | grep -q "suite 'totally-unmapped-suite' has no PackageSource mapping"
rm -rf "$tmp"
done
}
@test "validate sees a suite nested below a top-level directory" {
# Chainsaw walks every directory under the one it is handed, so a test in a
# subdirectory runs as part of its top-level directory, which then needs a
# mapping like any other suite.
tmp=$(mktemp -d)
mkdir -p "$tmp/suites/totally-unmapped-suite/cases"
: > "$tmp/suites/totally-unmapped-suite/cases/chainsaw-test.yaml"
err=$(hack/select-install.sh --validate packages/core/platform/sources "$tmp/suites" 2>&1 1>/dev/null) && {
echo "expected validation to fail on an unmapped nested suite" >&2
exit 1
}
echo "$err" | grep -q "suite 'totally-unmapped-suite' has no PackageSource mapping"
rm -rf "$tmp"
}
@test "validate fails when the suite listing itself fails" {
# A failed find and an empty suites dir used to read the same: the error
# went to /dev/null and the pipeline reported sort's status, so the mapping
# check validated nothing and passed.
tmp=$(mktemp -d)
mkdir "$tmp/bin"
printf '#!/bin/sh\necho "find: broken fixture" >&2\nexit 1\n' > "$tmp/bin/find"
chmod +x "$tmp/bin/find"
err=$(PATH="$tmp/bin:$PATH" hack/select-install.sh --validate 2>&1 1>/dev/null) && {
echo "expected validate to fail when find fails" >&2
exit 1
}
echo "$err" | grep -q "select-install: find failed listing the suites"
rm -rf "$tmp"
}
@test "validate detects a suite mapped to a source absent from the graph" {
# suite_to_source() hardcodes securitygroup -> cozystack.securitygroup-controller;
# a sources dir without that PackageSource must fail validation (fail closed),
# pinning that the elif validates every mapping, including hardcoded ones,
# against the graph.
tmp=$(mktemp -d)
mkdir -p "$tmp/sources" "$tmp/suites/securitygroup"
: > "$tmp/suites/securitygroup/chainsaw-test.yaml"
cat > "$tmp/sources/standalone.yaml" <<'YAML'
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.standalone
spec:
variants:
- name: default
YAML
err=$(hack/select-install.sh --validate "$tmp/sources" "$tmp/suites" 2>&1 1>/dev/null) && {
echo "expected validation to fail on a mapping to a missing source" >&2
exit 1
}
echo "$err" | grep -q "maps to 'cozystack.securitygroup-controller', not a PackageSource"
rm -rf "$tmp"
}
@test "validate fails when the suites dir does not exist" {
tmp=$(mktemp -d)
# real graph passes the graph checks; a missing suites dir must still FAIL
# (find would yield nothing and silently pass the mapping check otherwise).
if hack/select-install.sh --validate packages/core/platform/sources "$tmp/does-not-exist" 2>/dev/null; then
echo "expected validate to fail on a missing suites dir" >&2
exit 1
fi
rm -rf "$tmp"
}
@test "closure fails closed when the engine PackageSource is absent" {
tmp=$(mktemp -d)
mkdir -p "$tmp/sources"
# a lone app source, with no cozystack.cozystack-engine in the graph
cat > "$tmp/sources/foo-application.yaml" <<'YAML'
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.foo-application
spec:
variants:
- name: default
YAML
if hack/select-install.sh "foo" "$tmp/sources" 2>/dev/null; then
echo "expected a non-zero exit when the engine source is missing" >&2
exit 1
fi
rm -rf "$tmp"
}