Files
kernalix7--winpodx/tests/test_install_sh_provision.py
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:32:37 +08:00

244 lines
11 KiB
Python
Raw Permalink 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.
# 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}"