Files
ServerKit/scripts/test/test_provision_python.sh
T
Juan DenisandClaude Opus 5 4e80d0655f test(install): exercise Python provisioning against real distro repos
Seven distro images were green throughout issue #99 because every one of them
runs the unit suites against STUBS. That is the right trade for logic, but it
left the one thing that talks to real package repos completely untested — so a
new Debian release dropping the Python we ask for could only ever be found by a
user.

scripts/test/test_provision_python.sh runs identify_system, choose_pkg_manager
and provision_python for real, then proves the result is usable: in range, can
create a virtualenv, has working pip, and imports ssl/sqlite3/ctypes. It needs
no systemd, nginx or ports, so it fits the plain containers the matrix already
pulls. Reaching the source build counts as a FAILURE, not a pass: a distro that
gets there is one we have not really added support for.

Wired up as a new `python-provisioning` job over the same images, and debian:13
added to both matrices. Adding trixie to the existing matrix alone would have
been decorative — those legs would have gone green on a distro that cannot
install.

Proven in both directions before being relied on: against the pre-fix tree it
reproduced #99 exactly on debian:13 (the reporter's apt errors, the fall
through to a source build, and the abort message), while debian:12 passed 7/7.

Also fixed the suite pinning the bug. T2 asserted `ver_in_range 3.13` must be
FALSE and T1's stub used 3.13 as its "too new" case, so the gate could not be
widened without the tests failing. Both moved to 3.14, plus a direct guard on
PYTHON_MAX — otherwise a regression to 3.12 could be "fixed" by editing the
assertion to match. 122 passed, 0 failed.

full-stack-test.ps1 documents why debian13 is NOT in the VM harness: no trixie
box publishes a Hyper-V provider (generic/debian13 does not exist,
debian/trixie64 is libvirt-only, bento/debian-13 has no hyperv) and Multipass
is Ubuntu-only. Named there with what remains uncovered on trixie — the full
systemd + nginx + API end-to-end — rather than left as a silent gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 11:55:47 -04:00

157 lines
6.5 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
#
# Does install.sh actually obtain a working Python ON THIS DISTRO?
#
# Every other suite here runs against stubs, which is the right trade for logic
# but means the one thing that talks to the distro's real package repos was
# never exercised. Issue #99: Debian 13 ships only python3.13, the version gate
# accepted 3.11-3.12, so both `apt-get install python3.11` and `python3.12`
# failed and the installer fell through to a source build that cannot work
# (nothing in install.sh ever installs a compiler). Seven distro images in CI
# and none of them caught it, because none of them ran this code for real.
#
# So: run identify_system + choose_pkg_manager + provision_python against the
# real repos, then prove the result is usable. No systemd, no nginx, no network
# services — this fits in the plain distro containers the matrix already pulls.
#
# Destructive: installs packages. Intended for a container or a throwaway VM.
# Refuses to run on a box that looks like somebody's machine unless forced.
#
# Run: bash scripts/test/test_provision_python.sh
# SERVERKIT_ALLOW_DESTRUCTIVE=1 bash scripts/test/test_provision_python.sh
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
INSTALL_SH="$REPO_DIR/install.sh"
PASS=0
FAIL=0
SKIP=0
ok() { PASS=$((PASS + 1)); printf ' \033[32m✔\033[0m %s\n' "$1"; }
bad() { FAIL=$((FAIL + 1)); printf ' \033[31m✘\033[0m %s\n' "$1"; }
skip() { SKIP=$((SKIP + 1)); printf ' \033[33m∼\033[0m %s (skipped)\n' "$1"; }
# --------------------------------------------------------------------------
# Guard rails. This one really does apt/dnf install.
# --------------------------------------------------------------------------
if [ "${SERVERKIT_ALLOW_DESTRUCTIVE:-0}" != "1" ] \
&& [ ! -f /.dockerenv ] && [ -z "${CI:-}" ]; then
printf 'Refusing to run: this installs packages system-wide.\n' >&2
printf 'Run it in a container, or set SERVERKIT_ALLOW_DESTRUCTIVE=1.\n' >&2
exit 2
fi
if [ "$(id -u)" != "0" ]; then
printf 'Refusing to run: package installation needs root.\n' >&2
exit 2
fi
printf '\nprovision_python against real package repos\n'
# shellcheck source=/dev/null
source "$INSTALL_SH" >/dev/null 2>&1
# install.sh runs `set -euo pipefail` at the top, and sourcing applies that to
# THIS shell. Under -e the first probe that returns non-zero (which is the
# normal way half of these functions report "not found") would kill the run
# before printing a single assertion. Put it back the way a test harness needs.
set +e
# The installer's own detection, not a guess — provision_python branches on
# OS_FAMILY and ID, and choose_pkg_manager is what makes pkg_add do anything.
identify_system >/dev/null 2>&1 || true
choose_pkg_manager >/dev/null 2>&1 || true
printf ' distro: %s (ID=%s, family=%s, pkg=%s)\n' \
"${PRETTY_NAME:-unknown}" "${ID:-?}" "${OS_FAMILY:-?}" "${PKG_MGR:-?}"
printf ' gate: Python %s-%s\n' "$PYTHON_MIN" "$PYTHON_MAX"
if [ "${PKG_MGR:-}" = "" ]; then
skip "no package manager detected — nothing to provision with"
printf '\n%d passed, %d failed, %d skipped\n\n' "$PASS" "$FAIL" "$SKIP"
[ "$FAIL" -eq 0 ]
exit $?
fi
# --------------------------------------------------------------------------
# 1. A supported interpreter must be obtainable from the distro's own repos.
#
# The source-build fallback is deliberately NOT accepted as a pass here. It
# takes 20+ minutes with --enable-optimizations, needs a toolchain the
# installer never installs, and swallows its own errors through `| tail -1`.
# A distro that reaches it is a distro we have not really added support for.
# --------------------------------------------------------------------------
before_build_from_source=0
build_python_from_source() {
before_build_from_source=1
return 1
}
# provision_python's last line is `... || halt "Could not install a supported
# Python"`, and halt exits. Unstubbed it would take this harness down with it
# before a single assertion printed, turning a real failure into silence.
# Returning 1 instead lets provision_python report failure normally.
halted=""
halt() {
halted="$*"
return 1
}
if provision_python >/tmp/pp.log 2>&1; then
prov_rc=0
else
prov_rc=$?
fi
sed 's/^/ | /' /tmp/pp.log | tail -25
if [ "$before_build_from_source" = "1" ]; then
bad "no packaged Python in $PYTHON_MIN-$PYTHON_MAX on ${ID:-this distro} — the installer resorts to compiling one"
[ -n "$halted" ] && printf ' installer would abort with: %s\n' "$halted"
elif [ "$prov_rc" != "0" ]; then
bad "provision_python failed (exit $prov_rc)${halted:+ — $halted}"
else
ok "provision_python obtained a supported Python from the distro's packages"
fi
# --------------------------------------------------------------------------
# 2. Whatever it picked has to actually be usable.
# A python that cannot build a venv or run pip fails the install later, at a
# much more confusing moment.
# --------------------------------------------------------------------------
if [ -z "${PYTHON_BIN:-}" ] || ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
bad "PYTHON_BIN is unset or not on PATH after provision_python"
else
pyver="$("$PYTHON_BIN" -c 'import sys;print(".".join(map(str,sys.version_info[:2])))' 2>/dev/null || echo '?')"
if ver_in_range "$pyver"; then
ok "$PYTHON_BIN reports $pyver, inside the supported range"
else
bad "$PYTHON_BIN reports $pyver, outside $PYTHON_MIN-$PYTHON_MAX"
fi
if "$PYTHON_BIN" -m venv /tmp/pp-venv >/dev/null 2>&1 \
&& [ -x /tmp/pp-venv/bin/python ]; then
ok "it can create a virtualenv"
if /tmp/pp-venv/bin/python -m pip --version >/dev/null 2>&1; then
ok "the virtualenv has a working pip"
else
bad "the virtualenv has no working pip (missing ensurepip?)"
fi
# ssl and sqlite3 are the two stdlib modules a hand-built or
# stripped-down Python most often lacks, and both fail far away from
# here: no ssl means every HTTPS fetch dies, no sqlite3 means the
# database never opens.
for mod in ssl sqlite3 ctypes; do
if /tmp/pp-venv/bin/python -c "import $mod" >/dev/null 2>&1; then
ok "stdlib $mod is present"
else
bad "stdlib $mod is MISSING — the panel will fail later, not here"
fi
done
else
bad "it cannot create a virtualenv"
fi
rm -rf /tmp/pp-venv
fi
printf '\n%d passed, %d failed, %d skipped\n\n' "$PASS" "$FAIL" "$SKIP"
[ "$FAIL" -eq 0 ]