3e779be6f3
CI / lint (push) Failing after 13m4s
CI / test (3.11, ubuntu-latest) (push) Failing after 2m4s
CI / test (3.13, ubuntu-latest) (push) Successful in 13m30s
CI / test (3.14, ubuntu-latest) (push) Successful in 17m21s
CI / test (3.12, ubuntu-latest) (push) Successful in 17m55s
CI / discover-apps-ps (push) Successful in 1m56s
CI / test (3.9, ubuntu-latest) (push) Successful in 13m17s
CI / test (3.10, ubuntu-latest) (push) Successful in 26m21s
CI / audit (push) Successful in 13m38s
Deploy site / deploy (push) Has been cancelled
CI / test (3.14, ubuntu-24.04-arm) (push) Has been cancelled
244 lines
11 KiB
Python
244 lines
11 KiB
Python
# SPDX-License-Identifier: MIT
|
||
"""Parse-and-grep guard for install.sh's post-create provisioning (0.6.0 item B).
|
||
|
||
After the provision-unify cleanup, install.sh's post-`winpodx setup`
|
||
provisioning is the single ``winpodx provision`` command — NOT a bash copy
|
||
of the wait-ready → /health poll → 6× discovery retry → host-open chain.
|
||
These tests fail loudly if a regression reintroduces an inline bash copy,
|
||
which is exactly the duplication the unification killed.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
_INSTALL_SH = Path(__file__).resolve().parent.parent / "install.sh"
|
||
|
||
|
||
def _strip_comments(text: str) -> str:
|
||
"""Drop comment lines so prose explaining the unification doesn't trip the
|
||
grep guards. A comment line is one whose first non-space char is ``#``
|
||
(the shebang counts as a comment too). Inline trailing comments are rare
|
||
in install.sh and not load-bearing for these assertions, so a line-level
|
||
strip is sufficient and avoids mangling quoted ``#`` inside strings."""
|
||
kept = []
|
||
for line in text.splitlines():
|
||
if line.lstrip().startswith("#"):
|
||
continue
|
||
kept.append(line)
|
||
return "\n".join(kept)
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def script() -> str:
|
||
assert _INSTALL_SH.is_file(), f"install.sh not found at {_INSTALL_SH}"
|
||
return _strip_comments(_INSTALL_SH.read_text(encoding="utf-8"))
|
||
|
||
|
||
def test_invokes_winpodx_provision(script: str) -> None:
|
||
# The post-create step runs a single winpodx command chosen by the
|
||
# fresh/upgrade branch (PROVISION_CMD array), invoked through the symlink.
|
||
assert "PROVISION_CMD=(provision --require-agent)" in script
|
||
assert '"$SYMLINK" "${PROVISION_CMD[@]}"' in script
|
||
|
||
|
||
def test_installs_podman_compose_with_podman(script: str) -> None:
|
||
# #580/#503: winpodx hard-requires the standalone podman-compose, which the
|
||
# podman package doesn't pull in. install.sh must detect it and add it to
|
||
# the install set so pod creation doesn't fail with "compose command not
|
||
# found".
|
||
assert "PODMAN_COMPOSE_PRESENT" in script
|
||
assert 'MISSING+=("podman-compose")' in script
|
||
|
||
|
||
def test_atomic_rpm_path_honours_explicit_source_override(script: str) -> None:
|
||
# #548: on Atomic (rpm-ostree) install.sh defaults to the OBS RPM, but an
|
||
# explicit --main/--ref/--source/--image-tar must win and fall through to
|
||
# the git/venv flow (the OBS RPM only ships tagged releases). The gate must
|
||
# therefore require all three override vars to be empty before taking the
|
||
# rpm-ostree path.
|
||
lines = script.splitlines()
|
||
# The gate opens with `if command -v rpm-ostree ... \` and continues the
|
||
# compound `&& [ -z ... ]` condition on the next line.
|
||
gate = next(
|
||
(i for i, ln in enumerate(lines) if ln.lstrip().startswith("if command -v rpm-ostree")),
|
||
None,
|
||
)
|
||
assert gate is not None, "rpm-ostree gate must open the Atomic install path"
|
||
cond = " ".join(lines[gate : gate + 2])
|
||
assert '-z "$WINPODX_REF"' in cond
|
||
assert '-z "$WINPODX_SOURCE"' in cond
|
||
assert '-z "$WINPODX_IMAGE_TAR"' in cond
|
||
|
||
|
||
def test_no_inline_health_curl_poll(script: str) -> None:
|
||
# The 30× `curl .../health` settle poll moved into finish_provisioning.
|
||
# (install.sh still uses curl elsewhere — e.g. the GitHub release check —
|
||
# so we assert the absence of the agent /health endpoint specifically,
|
||
# not curl in general.)
|
||
assert "/health" not in script
|
||
assert "127.0.0.1:8765" not in script
|
||
assert "8765/health" not in script
|
||
|
||
|
||
def test_no_inline_six_times_discovery_retry(script: str) -> None:
|
||
# The 6× `app refresh` retry loop is gone from bash.
|
||
assert "for attempt in 1 2 3 4 5 6" not in script
|
||
assert '"$SYMLINK" app refresh' not in script
|
||
|
||
|
||
def test_no_inline_host_open_listener_start(script: str) -> None:
|
||
# Reverse-open listener start + refresh moved into finish_provisioning.
|
||
assert "host-open start-listener" not in script
|
||
assert "host-open refresh" not in script
|
||
|
||
|
||
def test_fresh_uses_provision_require_agent(script: str) -> None:
|
||
# Fresh install (no prior config): provision with the agent-first gate
|
||
# (#271) so discovery/apply defer instead of racing FreeRDP into
|
||
# install.bat's autologon session.
|
||
assert "PROVISION_CMD=(provision --require-agent)" in script
|
||
|
||
|
||
def test_upgrade_uses_migrate(script: str) -> None:
|
||
# Upgrade (prior config existed): migrate FIRST syncs the refreshed guest
|
||
# scripts (guest_sync) + pins the image, THEN runs the apply -> discovery
|
||
# -> reverse-open chain. A blind `provision`-for-both (the first item-B
|
||
# cut) left upgraded guests on stale agent.ps1 / OEM scripts.
|
||
assert "PROVISION_CMD=(migrate --non-interactive)" in script
|
||
|
||
|
||
def test_provision_step_branches_on_fresh_vs_upgrade(script: str) -> None:
|
||
# The two flows are gated on the pre-setup IS_FRESH_INSTALL snapshot.
|
||
assert 'if [ "$IS_FRESH_INSTALL" = "1" ]; then' in script
|
||
|
||
|
||
def test_create_only_flag_is_gone(script: str) -> None:
|
||
# setup --create-only was removed; install.sh must not pass it.
|
||
assert "--create-only" not in script
|
||
|
||
|
||
def test_setup_skips_its_own_provision_tail(script: str) -> None:
|
||
# install.sh runs the chain once via the explicit `winpodx provision`,
|
||
# so it tells `winpodx setup` to skip its own full-provision tail.
|
||
assert "WINPODX_NO_PROVISION=1" in script
|
||
|
||
|
||
def test_provision_call_disarms_err_trap(script: str) -> None:
|
||
# bash fires the ERR trap on a failing *pipeline* even under `set +e`, so
|
||
# the provision call must explicitly `trap - ERR` before it and re-arm
|
||
# `trap rollback_and_exit_err ERR` after — otherwise a deferred (exit 5)
|
||
# provision rolls back the whole fresh install before the rc handling runs.
|
||
assert "trap - ERR" in script
|
||
assert "trap rollback_and_exit_err ERR" in script
|
||
# Re-arm must come back after the disarm (ordering sanity).
|
||
assert script.index("trap - ERR") < script.rindex("trap rollback_and_exit_err ERR")
|
||
|
||
|
||
def test_deferred_provision_is_not_a_rollback(script: str) -> None:
|
||
# Exit 4 (wait-ready ran long) / 5 (agent-first discovery deferred) are
|
||
# NOT failures: Windows is downloaded + booted, so they record pending and
|
||
# keep the install instead of rolling back ~15 min of ISO download.
|
||
assert '[ "$PROVISION_RC" -eq 4 ] || [ "$PROVISION_RC" -eq 5 ]' in script
|
||
# The deferred branch points the user at the recovery command.
|
||
assert "winpodx app refresh" in script
|
||
|
||
|
||
def test_mode_prompt_reads_from_tty_so_curl_bash_can_choose(script: str) -> None:
|
||
# Under `curl ... | bash`, stdin is the script pipe, so the R/A/C/N mode
|
||
# menu (and Custom sub-prompts) must read from the controlling terminal
|
||
# /dev/tty — otherwise the menu is unreachable via the canonical install
|
||
# path and silently defaults to Recommended.
|
||
assert "true </dev/tty" in script # interactivity also detected via /dev/tty
|
||
assert "TTY_DEV" in script
|
||
# Every interactive prompt reads from $TTY_DEV, not bare stdin.
|
||
for bare in (
|
||
"read -r mode_answer\n",
|
||
"read -r be_answer\n",
|
||
"read -r gui_answer\n",
|
||
"read -r answer\n",
|
||
):
|
||
assert bare not in script, f"prompt still reads bare stdin: {bare!r}"
|
||
assert script.count('read -r mode_answer < "$TTY_DEV"') == 1
|
||
|
||
|
||
def test_custom_mode_picks_freerdp_source(script: str) -> None:
|
||
# Custom mode lets the user pick the FreeRDP client source (native/flatpak/
|
||
# auto), not just the backend + GUI.
|
||
assert "WINPODX_FREERDP_SOURCE" in script
|
||
assert '[ "$INSTALL_MODE" = "c" ]' in script
|
||
# The resolved source is handed to setup so the launcher honours it.
|
||
assert "--freerdp-source" in script
|
||
|
||
|
||
def test_freerdp_flatpak_not_installed_redundantly(script: str) -> None:
|
||
# When a FreeRDP client (native or Flatpak) is already present, no client is
|
||
# pulled. With NO client present, auto installs the NATIVE package (native
|
||
# is preferred); only an explicit `--freerdp-source flatpak` installs the
|
||
# Flatpak (INSTALL_FREERDP_FLATPAK).
|
||
assert "FREERDP_FLATPAK_PRESENT" in script
|
||
assert "FREERDP_NATIVE_PRESENT" in script
|
||
assert "INSTALL_FREERDP_FLATPAK" in script
|
||
assert "com.freerdp.FreeRDP" in script
|
||
|
||
|
||
def test_prints_install_plan_before_acting(script: str) -> None:
|
||
# After the mode + dependency sources are resolved, install.sh prints a
|
||
# plan summary (mode / backend / FreeRDP action / GUI / packages / VM)
|
||
# before installing anything, so the run is transparent.
|
||
assert "install plan" in script
|
||
# The plan must be emitted before the package-install loop runs.
|
||
assert script.index("install plan") < script.index("if [ ${#MISSING[@]} -gt 0 ]; then")
|
||
|
||
|
||
def test_upgrade_skips_the_mode_prompt(script: str) -> None:
|
||
# On an upgrade / re-run (a config already exists) the R/A/C/N mode prompt
|
||
# is pointless — install.sh reuses the existing config and runs migrate,
|
||
# so the prompt is gated on IS_FRESH_INSTALL.
|
||
assert '[ "$IS_FRESH_INSTALL" != "1" ]' in script
|
||
# The mode-prompt heredoc must come after that upgrade gate.
|
||
assert script.index('[ "$IS_FRESH_INSTALL" != "1" ]') < script.index("Install mode?")
|
||
|
||
|
||
def test_too_old_podman_guard_blocks_before_install(script: str) -> None:
|
||
# #271 ask 3: when the resolved backend is podman but podman is < 4 (Ubuntu
|
||
# 22.04 ships 3.4), Recommended mode / explicit --backend podman would
|
||
# otherwise proceed and fail at provisioning AFTER installing packages. A
|
||
# guard refuses to blindly continue — and it must run BEFORE any package
|
||
# install so an abort leaves the system unmodified.
|
||
guard = '[ "$WINPODX_ALLOW_OLD_PODMAN" != "1" ]'
|
||
assert guard in script
|
||
# Gated on the podman-too-old condition + the podman backend.
|
||
assert '[ "$WINPODX_BACKEND" = "podman" ] && [ "$PODMAN_TOO_OLD" = true ]' in script
|
||
# The guard sits before the install-plan / package-install phase.
|
||
assert script.index(guard) < script.index("install plan")
|
||
|
||
|
||
def test_too_old_podman_guard_has_override(script: str) -> None:
|
||
# An out-of-band podman upgrade can make the probe stale, so the guard is
|
||
# bypassable via env var and a matching flag.
|
||
assert "WINPODX_ALLOW_OLD_PODMAN" in script
|
||
assert "--allow-old-podman" in script
|
||
|
||
|
||
def test_too_old_podman_noninteractive_exits_clean(script: str) -> None:
|
||
# Non-interactive runs can't prompt, so the guard exits cleanly with
|
||
# guidance rather than silently switching backend or proceeding.
|
||
assert "aborted before modifying the system" in script.lower()
|
||
|
||
|
||
def test_no_inline_chain_steps_survive(script: str) -> None:
|
||
"""After `winpodx setup`, the post-create chain is driven by ONE winpodx
|
||
command (provision on fresh, migrate on upgrade, both via PROVISION_CMD).
|
||
The individual chain commands the old ~140-line bash copy invoked
|
||
directly must NOT survive as separate `"$SYMLINK" <cmd>` calls."""
|
||
forbidden = (
|
||
'"$SYMLINK" pod wait-ready',
|
||
'"$SYMLINK" app refresh',
|
||
'"$SYMLINK" host-open',
|
||
)
|
||
for cmd in forbidden:
|
||
assert cmd not in script, f"inline bash chain step still present: {cmd!r}"
|