Skip to content

Commit 09cf05e

Browse files
rtibblesbotclaude
andcommitted
Route apt-repo tests through explicit named assertions
Add tests/lib.sh with assert_file/assert_contains/assert_equals/ assert_files_equal/assert_output_contains helpers and a require_tools guard, and refactor the five apt-repo tests to use them so each check is an explicit, named assertion. require_tools honours APT_REPO_TESTS_STRICT, which the CI runner now sets so a missing-tool SKIP becomes a hard failure instead of a vacuous pass -- guaranteeing the assertions actually execute in CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5ddeaec commit 09cf05e

7 files changed

Lines changed: 124 additions & 60 deletions

File tree

.github/workflows/platform-apt-repo-test.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,16 @@ jobs:
3030
sudo apt-get update
3131
sudo apt-get install -y --no-install-recommends reprepro gnupg dpkg-dev rsync
3232
- name: Run the apt-repo tests
33+
# APT_REPO_TESTS_STRICT=1 turns a missing-tool SKIP into a hard failure,
34+
# so every test must actually run its assertions here rather than
35+
# passing vacuously — the tooling above (plus preinstalled docker) is
36+
# exactly what they need.
37+
env:
38+
APT_REPO_TESTS_STRICT: "1"
3339
run: |
3440
set -e
3541
for t in platforms/apt-repo/tests/*.sh; do
42+
[ "$(basename "$t")" = "lib.sh" ] && continue # shared helpers, not a test
3643
echo "::group::$t"
3744
sh "$t"
3845
echo "::endgroup::"

platforms/apt-repo/tests/e2e_cutover.sh

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
# resolves to the NEW repo at the docroot root — exactly the production cutover.
1515
set -eu
1616

17-
command -v docker >/dev/null 2>&1 || { echo "SKIP: needs docker"; exit 0; }
18-
1917
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
18+
. "$HERE/lib.sh"
19+
20+
require_tools docker
21+
2022
REPO_ROOT=$(CDPATH= cd -- "$HERE/../../.." && pwd)
2123

2224
# The container builds both repos, serves them, installs the cutover release, and
@@ -147,10 +149,10 @@ INSTALLED_NEW=$(dpkg-query -W -f='${Version}' kolibri-server)
147149
148150
echo "PASS: old-source client auto-rewritten to apt.learningequality.org and upgraded 1.0 -> 1.0.1 with no manual action"
149151
EOF
150-
) || { printf '%s\n' "$RESULT" >&2; echo "FAIL: e2e cutover container run exited non-zero"; exit 1; }
152+
) || { printf '%s\n' "$RESULT" >&2; fail "e2e cutover container run exited non-zero"; }
151153
printf '%s\n' "$RESULT"
152154

153155
# Top-level assertion: the container must have reached its PASS line. Without
154156
# this, a silent early return inside the heredoc would go unnoticed.
155-
printf '%s\n' "$RESULT" | grep -q '^PASS:' \
156-
|| { echo "FAIL: e2e cutover did not reach its PASS assertion"; exit 1; }
157+
assert_output_contains "$RESULT" '^PASS:' \
158+
"e2e cutover did not reach its PASS assertion"

platforms/apt-repo/tests/lib.sh

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Shared assertion + tooling helpers for the apt-repo shell tests.
2+
#
3+
# Sourced by every test. Assertions print a "FAIL:" line and exit non-zero
4+
# when they do not hold, so a regression aborts the test instead of passing
5+
# silently.
6+
#
7+
# require_tools normally SKIPs (exit 0) when a tool is absent, so the suite is
8+
# still usable on a bare dev box. In CI the runner exports
9+
# APT_REPO_TESTS_STRICT=1, which turns a missing tool into a hard failure —
10+
# guaranteeing every test actually reaches its assertions there rather than
11+
# passing vacuously.
12+
13+
fail() { echo "FAIL: $1"; exit 1; }
14+
15+
# assert_file <path> [message]
16+
assert_file() {
17+
[ -f "$1" ] || fail "${2:-expected file to exist: $1}"
18+
}
19+
20+
# assert_contains <file> <extended-regex> [message]
21+
assert_contains() {
22+
grep -Eq "$2" "$1" || fail "${3:-expected /$2/ in $1}"
23+
}
24+
25+
# assert_absent <file-or-dir> <extended-regex> [message] — recursive
26+
assert_absent() {
27+
if grep -rEq "$2" "$1"; then fail "${3:-unexpected match for /$2/ under $1}"; fi
28+
}
29+
30+
# assert_equals <actual> <expected> [message]
31+
assert_equals() {
32+
[ "$1" = "$2" ] || fail "${3:-expected '$2', got '$1'}"
33+
}
34+
35+
# assert_output_contains <string> <extended-regex> [message]
36+
assert_output_contains() {
37+
printf '%s\n' "$1" | grep -Eq "$2" || fail "${3:-expected /$2/ in captured output}"
38+
}
39+
40+
# assert_output_absent <string> <extended-regex> [message]
41+
assert_output_absent() {
42+
if printf '%s\n' "$1" | grep -Eq "$2"; then fail "${3:-unexpected /$2/ in captured output}"; fi
43+
}
44+
45+
# assert_files_equal <a> <b> [message]
46+
assert_files_equal() {
47+
cmp -s "$1" "$2" || fail "${3:-files differ: $1 vs $2}"
48+
}
49+
50+
# require_tools <tool>... — SKIP (or FAIL under APT_REPO_TESTS_STRICT=1) if any absent
51+
require_tools() {
52+
for _t in "$@"; do
53+
command -v "$_t" >/dev/null 2>&1 && continue
54+
if [ "${APT_REPO_TESTS_STRICT:-0}" = "1" ]; then
55+
fail "required tool missing under APT_REPO_TESTS_STRICT: $_t (needs: $*)"
56+
fi
57+
echo "SKIP: needs $* (missing: $_t)"
58+
exit 0
59+
done
60+
}

platforms/apt-repo/tests/test_keyring_install.sh

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,19 @@
99
set -eu
1010

1111
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
12+
. "$HERE/lib.sh"
13+
1214
APT_REPO="$HERE/.."
1315
REPO_ROOT=$(CDPATH= cd -- "$APT_REPO/../.." && pwd)
1416

1517
# --- key-drift guard (runs without docker) ----------------------------------
1618
PI_KEY="$REPO_ROOT/platforms/raspberry-pi/files/learningequality.asc"
1719
KEYRING_KEY="$APT_REPO/keyring/kolibri-archive-keyring.asc"
18-
[ -f "$KEYRING_KEY" ] || { echo "FAIL: $KEYRING_KEY missing"; exit 1; }
19-
cmp -s "$PI_KEY" "$KEYRING_KEY" \
20-
|| { echo "FAIL: kolibri-archive-keyring.asc differs from the Pi trust key"; exit 1; }
20+
assert_file "$KEYRING_KEY" "$KEYRING_KEY missing"
21+
assert_files_equal "$PI_KEY" "$KEYRING_KEY" \
22+
"kolibri-archive-keyring.asc differs from the Pi trust key"
2123

22-
command -v docker >/dev/null 2>&1 || { echo "SKIP: needs docker"; exit 0; }
24+
require_tools docker
2325

2426
# --- build + install in a clean Debian 13 container -------------------------
2527
# The container builds the native package, installs it, and asserts the two
@@ -64,10 +66,10 @@ if echo "$OUT" | grep -Eiq 'Malformed|NO_PUBKEY|not signed|invalid|bad'; then
6466
fi
6567
echo "PASS: keyring builds, installs source + key, apt-get update parses it"
6668
EOF
67-
) || { printf '%s\n' "$RESULT" >&2; echo "FAIL: keyring container run exited non-zero"; exit 1; }
69+
) || { printf '%s\n' "$RESULT" >&2; fail "keyring container run exited non-zero"; }
6870
printf '%s\n' "$RESULT"
6971

7072
# Top-level assertion: the container must have reached its PASS line. Without
7173
# this, a silent early return inside the heredoc would go unnoticed.
72-
printf '%s\n' "$RESULT" | grep -q '^PASS:' \
73-
|| { echo "FAIL: keyring test did not reach its PASS assertion"; exit 1; }
74+
assert_output_contains "$RESULT" '^PASS:' \
75+
"keyring test did not reach its PASS assertion"

platforms/apt-repo/tests/test_migrate_apt_source.sh

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99
set -eu
1010

1111
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
12+
. "$HERE/lib.sh"
13+
1214
SNIPPET="$HERE/../migrate-apt-source.sh"
13-
[ -f "$SNIPPET" ] || { echo "FAIL: $SNIPPET missing"; exit 1; }
15+
assert_file "$SNIPPET" "$SNIPPET missing"
1416

1517
WORK=$(mktemp -d)
1618
trap 'rm -rf "$WORK"' EXIT
1719

18-
fail() { echo "FAIL: $1"; exit 1; }
19-
2020
# --- Case A: rewrite (AC#6) --------------------------------------------------
2121
# A deb822 .sources naming the old kolibri-server host, plus a legacy one-line
2222
# .list naming the old kolibri-installer-debian host. Both must move to the
@@ -36,13 +36,12 @@ EOF
3636

3737
KOLIBRI_APT_SOURCES_DIR="$A" sh "$SNIPPET"
3838

39-
grep -q 'apt.learningequality.org' "$A/learningequality-kolibri-server.sources" \
40-
|| fail "Case A: .sources not rewritten to apt.learningequality.org"
41-
grep -q 'apt.learningequality.org' "$A/learningequality-debian.list" \
42-
|| fail "Case A: .list not rewritten to apt.learningequality.org"
43-
if grep -rq 'learningequality\.github\.io' "$A"; then
44-
fail "Case A: a github.io host survived the rewrite"
45-
fi
39+
assert_contains "$A/learningequality-kolibri-server.sources" 'apt\.learningequality\.org' \
40+
"Case A: .sources not rewritten to apt.learningequality.org"
41+
assert_contains "$A/learningequality-debian.list" 'apt\.learningequality\.org' \
42+
"Case A: .list not rewritten to apt.learningequality.org"
43+
assert_absent "$A" 'learningequality\.github\.io' \
44+
"Case A: a github.io host survived the rewrite"
4645

4746
# --- Case B: no-op (AC#7) ----------------------------------------------------
4847
# A Launchpad-PPA source has no github.io host — the file must be byte-unchanged.
@@ -55,24 +54,24 @@ cp "$B/learningequality-ubuntu-kolibri.list" "$WORK/case_b_orig.list"
5554

5655
KOLIBRI_APT_SOURCES_DIR="$B" sh "$SNIPPET"
5756

58-
cmp -s "$B/learningequality-ubuntu-kolibri.list" "$WORK/case_b_orig.list" \
59-
|| fail "Case B: Launchpad PPA source was modified (should be no-op)"
57+
assert_files_equal "$B/learningequality-ubuntu-kolibri.list" "$WORK/case_b_orig.list" \
58+
"Case B: Launchpad PPA source was modified (should be no-op)"
6059

6160
# --- Case C: idempotent ------------------------------------------------------
6261
# A second run over Case A's already-rewritten fixture changes nothing.
6362
cp -a "$A" "$WORK/case_c"
6463
C="$WORK/case_c"
6564
snapshot=$(cat "$C"/* )
6665
KOLIBRI_APT_SOURCES_DIR="$C" sh "$SNIPPET"
67-
[ "$snapshot" = "$(cat "$C"/*)" ] \
68-
|| fail "Case C: second run mutated an already-rewritten fixture"
66+
assert_equals "$(cat "$C"/*)" "$snapshot" \
67+
"Case C: second run mutated an already-rewritten fixture"
6968

7069
# --- Case D: sourced under `set -e`, no match, must not abort (AC#7 path) -----
7170
# The postinst sources the snippet and calls the function under `set -e`; on a
7271
# no-match install grep exits 1 — that must not abort. Mimic the integration.
7372
KOLIBRI_APT_SOURCES_DIR="$B" sh -e -c ". '$SNIPPET'; migrate_kolibri_apt_source; echo OK" \
7473
> "$WORK/case_d.out" 2>&1 || fail "Case D: aborted under set -e on no-match"
75-
grep -qx 'OK' "$WORK/case_d.out" \
76-
|| fail "Case D: did not reach OK (sourced call aborted under set -e)"
74+
assert_contains "$WORK/case_d.out" '^OK$' \
75+
"Case D: did not reach OK (sourced call aborted under set -e)"
7776

7877
echo "PASS: rewrite, no-op, idempotent, and set -e no-match all green"

platforms/apt-repo/tests/test_publish_roundtrip.sh

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,11 @@
44
# only local tooling (no GCS).
55
set -eu
66

7-
for tool in reprepro gpg dpkg-deb rsync; do
8-
command -v "$tool" >/dev/null 2>&1 || {
9-
echo "SKIP: needs reprepro gnupg dpkg-dev rsync (missing: $tool)"
10-
exit 0
11-
}
12-
done
13-
147
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
8+
. "$HERE/lib.sh"
9+
10+
require_tools reprepro gpg dpkg-deb rsync
11+
1512
PUBLISH="$HERE/../publish.sh"
1613

1714
WORK=$(mktemp -d)
@@ -36,7 +33,7 @@ Expire-Date: 0
3633
EOF
3734
gpg --batch --gen-key "$WORK/key.spec" >/dev/null 2>&1
3835
FPR=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr:/ { print $10; exit }')
39-
[ -n "$FPR" ] || { echo "FAIL: could not create signing key"; exit 1; }
36+
[ -n "$FPR" ] || fail "could not create signing key"
4037
export REPREPRO_SIGN_KEY="$FPR"
4138

4239
# --- build two minimal arch-all .debs with distinct names -------------------
@@ -69,19 +66,19 @@ bash "$PUBLISH" "$WORK/bbb-test_1.0_all.deb"
6966

7067
# --- AC#3: both present after the second publish ----------------------------
7168
PKGS="$BUCKET/dists/stable/main/binary-amd64/Packages"
72-
[ -f "$PKGS" ] || { echo "FAIL: $PKGS missing"; exit 1; }
73-
grep -q "^Package: aaa-test$" "$PKGS" || { echo "FAIL: aaa-test dropped by second publish"; exit 1; }
74-
grep -q "^Package: bbb-test$" "$PKGS" || { echo "FAIL: bbb-test missing after second publish"; exit 1; }
69+
assert_file "$PKGS" "$PKGS missing"
70+
assert_contains "$PKGS" '^Package: aaa-test$' "aaa-test dropped by second publish"
71+
assert_contains "$PKGS" '^Package: bbb-test$' "bbb-test missing after second publish"
7572

7673
# --- AC#2: Release signed + pubkey.asc served -------------------------------
7774
{ [ -f "$BUCKET/dists/stable/Release.gpg" ] || [ -f "$BUCKET/dists/stable/InRelease" ]; } \
78-
|| { echo "FAIL: no signature (Release.gpg / InRelease) on the published Release"; exit 1; }
79-
[ -f "$BUCKET/pubkey.asc" ] || { echo "FAIL: pubkey.asc not served at bucket root"; exit 1; }
75+
|| fail "no signature (Release.gpg / InRelease) on the published Release"
76+
assert_file "$BUCKET/pubkey.asc" "pubkey.asc not served at bucket root"
8077

8178
# --- bootstrap-root path: kolibri-archive-keyring.deb at bucket root ---------
8279
KOLIBRI_KEYRING_DEB="$WORK/aaa-test_1.0_all.deb" bash "$PUBLISH" "$WORK/aaa-test_1.0_all.deb"
83-
[ -f "$BUCKET/kolibri-archive-keyring.deb" ] \
84-
|| { echo "FAIL: kolibri-archive-keyring.deb not served at bucket root"; exit 1; }
80+
assert_file "$BUCKET/kolibri-archive-keyring.deb" \
81+
"kolibri-archive-keyring.deb not served at bucket root"
8582

8683
# --- keyring re-publish across releases (fixed version, rebuilt bytes) --------
8784
# The keyring version stays fixed across releases but CI rebuilds the .deb every
@@ -107,6 +104,6 @@ build_keyring_variant a
107104
build_keyring_variant b
108105
KOLIBRI_KEYRING_DEB="$WORK/kr-keyring-a.deb" bash "$PUBLISH" "$WORK/aaa-test_1.0_all.deb"
109106
KOLIBRI_KEYRING_DEB="$WORK/kr-keyring-b.deb" bash "$PUBLISH" "$WORK/aaa-test_1.0_all.deb" \
110-
|| { echo "FAIL: keyring re-publish aborted on a rebuilt same-version .deb"; exit 1; }
107+
|| fail "keyring re-publish aborted on a rebuilt same-version .deb"
111108

112109
echo "PASS: round-trip preserves prior packages; Release signed; pubkey.asc + bootstrap deb served"

platforms/apt-repo/tests/test_seed_old_pages.sh

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,13 @@
77
# KOLIBRI_SEED_REMOTE_BASE points the script at it (no GitHub access).
88
set -eu
99

10-
for tool in reprepro gpg dpkg-deb git; do
11-
command -v "$tool" >/dev/null 2>&1 || {
12-
echo "SKIP: needs reprepro gnupg dpkg-dev git (missing: $tool)"
13-
exit 0
14-
}
15-
done
16-
1710
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
11+
. "$HERE/lib.sh"
12+
13+
require_tools reprepro gpg dpkg-deb git
14+
1815
SEED="$HERE/../seed_old_pages.sh"
19-
[ -f "$SEED" ] || { echo "FAIL: $SEED missing"; exit 1; }
16+
assert_file "$SEED" "$SEED missing"
2017

2118
WORK=$(mktemp -d)
2219
cleanup() {
@@ -40,7 +37,7 @@ Expire-Date: 0
4037
EOF
4138
gpg --batch --gen-key "$WORK/key.spec" >/dev/null 2>&1
4239
FPR=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr:/ { print $10; exit }')
43-
[ -n "$FPR" ] || { echo "FAIL: could not create signing key"; exit 1; }
40+
[ -n "$FPR" ] || fail "could not create signing key"
4441

4542
# --- build two minimal arch-all .debs ---------------------------------------
4643
build_deb() {
@@ -93,13 +90,13 @@ assert_branch_has_both() {
9390
chk="$WORK/check-$1"
9491
git clone --quiet --branch gh-pages --single-branch "$BARE" "$chk"
9592
P="$chk/dists/stable/main/binary-amd64/Packages"
96-
[ -f "$P" ] || { echo "FAIL ($1): $P missing on pushed branch"; exit 1; }
97-
grep -q "^Package: kolibri-server$" "$P" \
98-
|| { echo "FAIL ($1): seeded kolibri-server not on pushed branch"; exit 1; }
99-
grep -q "^Package: preexisting-pkg$" "$P" \
100-
|| { echo "FAIL ($1): pre-existing package dropped (rebuilt fresh?)"; exit 1; }
93+
assert_file "$P" "($1): $P missing on pushed branch"
94+
assert_contains "$P" '^Package: kolibri-server$' \
95+
"($1): seeded kolibri-server not on pushed branch"
96+
assert_contains "$P" '^Package: preexisting-pkg$' \
97+
"($1): pre-existing package dropped (rebuilt fresh?)"
10198
ls "$chk"/pool/main/*/*/kolibri-server_1.0_all.deb >/dev/null 2>&1 \
102-
|| { echo "FAIL ($1): kolibri-server .deb missing from pool/"; exit 1; }
99+
|| fail "($1): kolibri-server .deb missing from pool/"
103100
}
104101

105102
# --- first run: pushes the cutover .deb -------------------------------------
@@ -110,7 +107,7 @@ assert_branch_has_both run1
110107

111108
# --- second run: safe to run again (duplicate version is a non-fatal no-op) --
112109
bash "$SEED" --deb "$WORK/kolibri-server_1.0_all.deb" --repo kolibri-server \
113-
|| { echo "FAIL: second run exited non-zero (not safe to run again)"; exit 1; }
110+
|| fail "second run exited non-zero (not safe to run again)"
114111
assert_branch_has_both run2
115112

116113
echo "PASS: seed pushes cutover .deb, preserves prior package, safe to run again"

0 commit comments

Comments
 (0)