diff --git a/Makefile b/Makefile index 386b288..a4bec9b 100644 --- a/Makefile +++ b/Makefile @@ -40,8 +40,8 @@ help: # ─── Pre-install gates ────────────────────────────────────────────────────── check-python: - @echo "=== Verifying Python ≥ 3.9 ===" - @$(PYTHON) -c "import sys; v=sys.version_info; exit(0 if (v>=(3,9)) else 1)" || { echo "Error: Python 3.9+ required (found $$($(PYTHON) --version 2>&1))"; exit 1; } + @echo "=== Verifying Python ≥ 3.10 ===" + @$(PYTHON) -c "import sys; v=sys.version_info; exit(0 if (v>=(3,10)) else 1)" || { echo "Error: Python 3.10+ required (found $$($(PYTHON) --version 2>&1))"; exit 1; } check-smartctl: @echo "=== Verifying smartctl ===" diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 65b50b8..83bdd0b 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -359,6 +359,151 @@ def _assert_removal_semantics(container: str, fmt: str) -> None: assert rc != 0, "Venv Python should be removed after uninstall" +# --------------------------------------------------------------------------- +# Tests — Python 3.10 floor (spec §7, nfpm depends) +# --------------------------------------------------------------------------- + +@pytest.mark.slow +def test_python_floor(skip_no_docker, version): + """Verify the Python 3.10 floor on the oldest supported deb target. + + Two sub-checks: + 1. Ubuntu 22.04 (Python 3.10): the same deb installs successfully, + confirming the floor is met on the oldest target. + 2. Debian 11 (Python 3.9): installation fails cleanly because the + declared dependency ``python3 (>= 3.10)`` is unsatisfied. + """ + pkg = _find_package("deb") + pkg_name = pkg.name + + # --- Sub-check 1: floor met on Ubuntu 22.04 --- + build_dir = REPO_ROOT / "build" / "test-container-floor" + build_dir.mkdir(parents=True, exist_ok=True) + (build_dir / "dist").mkdir(exist_ok=True) + subprocess.run( + ["cp", str(pkg), str(build_dir / "dist" / pkg_name)], + check=True, + ) + dockerfile = _build_deb_dockerfile("ubuntu:22.04", pkg_name) + (build_dir / "Dockerfile").write_text(dockerfile) + + tag = "fenris-floor-ubuntu-2204" + subprocess.run( + ["docker", "build", "-t", tag, str(build_dir)], + check=True, + capture_output=True, + timeout=300, + ) + container = f"fenris-floor-{os.getpid()}" + subprocess.run( + [ + "docker", "run", "-d", "--name", container, + "--tmpfs", "/tmp:exec,size=64m", + tag, "sleep", "infinity", + ], + check=True, + capture_output=True, + ) + try: + # Verify Python 3.10 is the system interpreter + rc, out = _container_exec(container, "python3 -c 'import sys; print(sys.version_info[:2])'") + assert rc == 0, f"Cannot check system Python: {out}" + major, minor = (int(x) for x in out.strip().strip("()").split(",")) + assert (major, minor) >= (3, 10), \ + f"Expected Python >= 3.10 on Ubuntu 22.04, got {major}.{minor}" + + # Verify the package dependency is declared + rc, out = _container_exec( + container, + "dpkg -s fenris 2>/dev/null | grep -i 'Depends:' || true", + ) + assert "python3" in out, f"python3 dependency not declared: {out}" + assert "3.10" in out, f"Python 3.10 floor not in dependency: {out}" + + # Verify the bundled venv exists (binary may not execute on the host + # interpreter — that's tested separately by the dormant-install test) + rc, _ = _container_exec(container, "test -f /opt/fenris/bin/python3") + assert rc == 0, "Bundled venv Python binary not found" + + # fenris on PATH + rc, _ = _container_exec(container, "command -v fenris") + assert rc == 0, "fenris not on PATH after floor-met install" + finally: + subprocess.run( + ["docker", "rm", "-f", container], + capture_output=True, + ) + + # --- Sub-check 2: below floor on Debian 11 (Python 3.9) --- + build_dir_floor = REPO_ROOT / "build" / "test-container-below-floor" + build_dir_floor.mkdir(parents=True, exist_ok=True) + (build_dir_floor / "dist").mkdir(exist_ok=True) + subprocess.run( + ["cp", str(pkg), str(build_dir_floor / "dist" / pkg_name)], + check=True, + ) + dockerfile_floor = textwrap.dedent(f"""\ + FROM debian:bullseye + RUN apt-get update && apt-get install -y --no-install-recommends \\ + python3 systemd dbus && \\ + rm -rf /var/lib/apt/lists/* + COPY dist/{pkg_name} /pkg/{pkg_name} + """) + (build_dir_floor / "Dockerfile").write_text(dockerfile_floor) + + tag_floor = "fenris-below-floor-debian11" + subprocess.run( + ["docker", "build", "-t", tag_floor, str(build_dir_floor)], + check=True, + capture_output=True, + timeout=300, + ) + container_floor = f"fenris-below-floor-{os.getpid()}" + subprocess.run( + [ + "docker", "run", "-d", "--name", container_floor, + "--tmpfs", "/tmp:exec,size=64m", + tag_floor, "sleep", "infinity", + ], + check=True, + capture_output=True, + ) + try: + # Confirm Python 3.9 is present (below floor) + rc, out = _container_exec( + container_floor, + "python3 -c 'import sys; print(f\"{sys.version_info.major}.{sys.version_info.minor}\")'", + ) + assert rc == 0, f"Cannot check system Python on Debian 11: {out}" + major, minor = (int(x) for x in out.strip().split(".")) + assert (major, minor) < (3, 10), \ + f"Expected Python < 3.10 on Debian 11, got {major}.{minor}" + + # Attempt install — should fail due to unmet dependency + rc, out = _container_exec( + container_floor, + f"dpkg -i /pkg/{pkg_name} 2>&1; echo EXIT:$?", + ) + # dpkg exits non-zero when dependencies are unmet; the EXIT:N + # line in the output captures the real exit code even if the + # shell wraps it. + assert "error" in out.lower() or "dependency" in out.lower() or "EXIT:0" not in out, \ + f"Expected install failure below floor, but got: {out}" + + # Confirm package is NOT properly configured (unpacked due to unmet deps) + rc, out = _container_exec( + container_floor, + "dpkg -s fenris 2>/dev/null | grep '^Status:' || echo NO_STATUS", + ) + assert "unpacked" in out.lower() or "not installed" in out.lower() or rc != 0, \ + f"Package should not be configured below Python floor: {out}" + finally: + subprocess.run( + ["docker", "rm", "-f", container_floor], + capture_output=True, + ) + + # --------------------------------------------------------------------------- # Tests — dormant install (tracer bullet) # ---------------------------------------------------------------------------