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
903 lines
30 KiB
Python
903 lines
30 KiB
Python
# SPDX-License-Identifier: MIT
|
|
"""Tests for the 0.6.0 item G command taxonomy reorganisation.
|
|
|
|
Covers:
|
|
- Every old ``pod <x>`` alias still parses AND dispatches (emits deprecation
|
|
line, calls the shared handler).
|
|
- Every new canonical command (``guest``, ``install``) parses AND dispatches.
|
|
- ``pod start/stop/status/restart/recreate/wait-ready`` unchanged (no
|
|
deprecation).
|
|
- ``doctor --json`` emits valid JSON with the expected keys.
|
|
- ``doctor --quick`` skips the slow probe (_check_container_health), runs
|
|
cheap probes.
|
|
- ``winpodx --help`` lists ``pod``, ``guest``, ``install``, ``doctor``,
|
|
``provision``.
|
|
- ``winpodx info`` and ``winpodx check`` emit the deprecation notice.
|
|
- Grep assertions over install.sh / uninstall.sh (no old command literals).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from io import StringIO
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_REPO = Path(__file__).parent.parent
|
|
|
|
|
|
def _parse(argv: list[str]) -> argparse.Namespace | None:
|
|
"""Parse ``winpodx <argv>`` and return the Namespace without dispatching."""
|
|
from winpodx.cli.main import cli
|
|
|
|
captured: dict = {}
|
|
|
|
def _fake_dispatch(args: argparse.Namespace) -> None:
|
|
captured["args"] = args
|
|
|
|
with patch("winpodx.cli.main._dispatch", side_effect=_fake_dispatch):
|
|
try:
|
|
cli(argv)
|
|
except SystemExit:
|
|
pass
|
|
return captured.get("args")
|
|
|
|
|
|
def _run_cli(argv: list[str]) -> tuple[str, str, int]:
|
|
"""Run ``winpodx`` as a subprocess and return (stdout, stderr, returncode)."""
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "winpodx"] + argv,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
return result.stdout, result.stderr, result.returncode
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Part 1: doctor --json
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestDoctorJson:
|
|
def test_json_flag_produces_valid_json(self, monkeypatch):
|
|
"""``doctor --json`` must emit a JSON array parseable by json.loads."""
|
|
from winpodx.cli.doctor import Finding, handle_doctor
|
|
|
|
ok = Finding("ok", "stub-title", detail="d", suggestion="s")
|
|
for name in (
|
|
"_check_install_source",
|
|
"_check_freerdp",
|
|
"_check_kvm",
|
|
"_check_config_state",
|
|
"_check_pending_setup",
|
|
"_check_autostart_entry",
|
|
"_check_initialized_flag",
|
|
):
|
|
monkeypatch.setattr(f"winpodx.cli.doctor.{name}", lambda: ok)
|
|
monkeypatch.setattr("winpodx.cli.doctor._check_container_backend", lambda: [ok])
|
|
monkeypatch.setattr("winpodx.cli.doctor._check_container_health", lambda: [ok])
|
|
|
|
buf = StringIO()
|
|
with patch("sys.stdout", buf):
|
|
handle_doctor(argparse.Namespace(json=True, quick=False))
|
|
|
|
payload = json.loads(buf.getvalue())
|
|
assert isinstance(payload, list)
|
|
assert len(payload) > 0
|
|
first = payload[0]
|
|
assert set(first.keys()) >= {"severity", "title", "detail", "suggestion"}
|
|
|
|
def test_json_exits_one_on_fail(self, monkeypatch):
|
|
"""``doctor --json`` with a FAIL finding must still exit 1."""
|
|
from winpodx.cli.doctor import Finding, handle_doctor
|
|
|
|
fail = Finding("fail", "broken", detail="detail", suggestion="fix it")
|
|
ok = Finding("ok", "fine")
|
|
for name in (
|
|
"_check_install_source",
|
|
"_check_freerdp",
|
|
"_check_kvm",
|
|
"_check_config_state",
|
|
"_check_pending_setup",
|
|
"_check_autostart_entry",
|
|
"_check_initialized_flag",
|
|
):
|
|
monkeypatch.setattr(f"winpodx.cli.doctor.{name}", lambda: fail)
|
|
monkeypatch.setattr("winpodx.cli.doctor._check_container_backend", lambda: [ok])
|
|
monkeypatch.setattr("winpodx.cli.doctor._check_container_health", lambda: [ok])
|
|
|
|
buf = StringIO()
|
|
with patch("sys.stdout", buf):
|
|
with pytest.raises(SystemExit) as exc:
|
|
handle_doctor(argparse.Namespace(json=True, quick=False))
|
|
assert exc.value.code == 1
|
|
payload = json.loads(buf.getvalue())
|
|
assert any(f["severity"] == "fail" for f in payload)
|
|
|
|
def test_json_keys_present_for_every_finding(self, monkeypatch):
|
|
"""Every element in the JSON array must have all four required keys."""
|
|
from winpodx.cli.doctor import Finding, handle_doctor
|
|
|
|
ok = Finding("ok", "t", detail="", suggestion="")
|
|
for name in (
|
|
"_check_install_source",
|
|
"_check_freerdp",
|
|
"_check_kvm",
|
|
"_check_config_state",
|
|
"_check_pending_setup",
|
|
"_check_autostart_entry",
|
|
"_check_initialized_flag",
|
|
):
|
|
monkeypatch.setattr(f"winpodx.cli.doctor.{name}", lambda: ok)
|
|
monkeypatch.setattr("winpodx.cli.doctor._check_container_backend", lambda: [ok])
|
|
monkeypatch.setattr("winpodx.cli.doctor._check_container_health", lambda: [ok])
|
|
|
|
buf = StringIO()
|
|
with patch("sys.stdout", buf):
|
|
handle_doctor(argparse.Namespace(json=True, quick=False))
|
|
|
|
for item in json.loads(buf.getvalue()):
|
|
for key in ("severity", "title", "detail", "suggestion"):
|
|
assert key in item
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Part 2: doctor --quick
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestDoctorQuick:
|
|
def test_quick_skips_container_health(self, monkeypatch):
|
|
"""``doctor --quick`` must NOT call ``_check_container_health``."""
|
|
from winpodx.cli.doctor import Finding, handle_doctor
|
|
|
|
ok = Finding("ok", "stub")
|
|
for name in (
|
|
"_check_install_source",
|
|
"_check_freerdp",
|
|
"_check_kvm",
|
|
"_check_config_state",
|
|
"_check_pending_setup",
|
|
"_check_autostart_entry",
|
|
"_check_initialized_flag",
|
|
):
|
|
monkeypatch.setattr(f"winpodx.cli.doctor.{name}", lambda: ok)
|
|
monkeypatch.setattr("winpodx.cli.doctor._check_container_backend", lambda: [ok])
|
|
|
|
called = []
|
|
monkeypatch.setattr(
|
|
"winpodx.cli.doctor._check_container_health",
|
|
lambda: called.append(True) or [], # type: ignore[return-value]
|
|
)
|
|
|
|
buf = StringIO()
|
|
with patch("sys.stdout", buf):
|
|
handle_doctor(argparse.Namespace(json=False, quick=True))
|
|
|
|
assert called == [], "_check_container_health must not be called with --quick"
|
|
|
|
def test_quick_still_runs_cheap_checks(self, monkeypatch):
|
|
"""``doctor --quick`` must still run the cheap local probes."""
|
|
from winpodx.cli.doctor import Finding, handle_doctor
|
|
|
|
ok = Finding("ok", "stub")
|
|
called: list[str] = []
|
|
|
|
def _make_probe(name: str):
|
|
def _probe():
|
|
called.append(name)
|
|
return ok
|
|
|
|
return _probe
|
|
|
|
cheap_names = (
|
|
"_check_install_source",
|
|
"_check_freerdp",
|
|
"_check_kvm",
|
|
"_check_config_state",
|
|
"_check_pending_setup",
|
|
"_check_autostart_entry",
|
|
"_check_initialized_flag",
|
|
)
|
|
for n in cheap_names:
|
|
monkeypatch.setattr(f"winpodx.cli.doctor.{n}", _make_probe(n))
|
|
monkeypatch.setattr("winpodx.cli.doctor._check_container_backend", lambda: [ok])
|
|
monkeypatch.setattr("winpodx.cli.doctor._check_container_health", lambda: [])
|
|
|
|
buf = StringIO()
|
|
with patch("sys.stdout", buf):
|
|
handle_doctor(argparse.Namespace(json=False, quick=True))
|
|
|
|
for n in cheap_names:
|
|
assert n in called, f"{n} should have been called in --quick mode"
|
|
|
|
def test_quick_not_quick_includes_container_health(self, monkeypatch):
|
|
"""Without ``--quick``, ``_check_container_health`` IS called."""
|
|
from winpodx.cli.doctor import Finding, handle_doctor
|
|
|
|
ok = Finding("ok", "stub")
|
|
for name in (
|
|
"_check_install_source",
|
|
"_check_freerdp",
|
|
"_check_kvm",
|
|
"_check_config_state",
|
|
"_check_pending_setup",
|
|
"_check_autostart_entry",
|
|
"_check_initialized_flag",
|
|
):
|
|
monkeypatch.setattr(f"winpodx.cli.doctor.{name}", lambda: ok)
|
|
monkeypatch.setattr("winpodx.cli.doctor._check_container_backend", lambda: [ok])
|
|
|
|
called = []
|
|
monkeypatch.setattr(
|
|
"winpodx.cli.doctor._check_container_health",
|
|
lambda: called.append(True) or [], # type: ignore[return-value]
|
|
)
|
|
|
|
buf = StringIO()
|
|
with patch("sys.stdout", buf):
|
|
handle_doctor(argparse.Namespace(json=False, quick=False))
|
|
|
|
assert called, "_check_container_health should be called without --quick"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Part 3: deprecation of info / check
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestInfoDeprecation:
|
|
def test_info_prints_deprecation_to_stderr(self, capsys, monkeypatch):
|
|
"""``winpodx info`` must print a deprecation notice to stderr."""
|
|
# Stub _cmd_info so we don't need a real config
|
|
monkeypatch.setattr("winpodx.cli.main._cmd_info", lambda: None)
|
|
monkeypatch.setattr("winpodx.cli.main._maybe_resume_pending", lambda _: None)
|
|
monkeypatch.setattr(
|
|
"winpodx.cli.main.maybe_run_first_run_prompt",
|
|
lambda _: None,
|
|
raising=False,
|
|
)
|
|
|
|
args = argparse.Namespace(command="info")
|
|
from winpodx.cli.main import _dispatch
|
|
|
|
_dispatch(args)
|
|
err = capsys.readouterr().err
|
|
assert "[deprecated]" in err
|
|
assert "winpodx info" in err
|
|
assert "0.7.0" in err
|
|
assert "winpodx doctor" in err
|
|
|
|
def test_check_prints_deprecation_to_stderr(self, capsys, monkeypatch):
|
|
"""``winpodx check`` must print a deprecation notice to stderr."""
|
|
monkeypatch.setattr("winpodx.cli.main._cmd_check", lambda _args: 0, raising=False)
|
|
monkeypatch.setattr("winpodx.cli.main._maybe_resume_pending", lambda _: None)
|
|
|
|
args = argparse.Namespace(command="check", json=False)
|
|
from winpodx.cli.main import _dispatch
|
|
|
|
with pytest.raises(SystemExit):
|
|
_dispatch(args)
|
|
err = capsys.readouterr().err
|
|
assert "[deprecated]" in err
|
|
assert "winpodx check" in err
|
|
assert "winpodx doctor" in err
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Part 4: pod lifecycle commands — no deprecation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestPodLifecycleNoDeprecation:
|
|
"""start/stop/status/restart/recreate/wait-ready must NOT emit deprecation."""
|
|
|
|
@pytest.mark.parametrize(
|
|
"argv,extra_attrs",
|
|
[
|
|
(["pod", "start"], {"wait": False, "timeout": 300}),
|
|
(["pod", "stop"], {}),
|
|
(["pod", "status"], {}),
|
|
(["pod", "restart"], {}),
|
|
],
|
|
)
|
|
def test_lifecycle_command_parses(self, argv, extra_attrs):
|
|
ns = _parse(argv)
|
|
assert ns is not None
|
|
assert ns.pod_command == argv[1]
|
|
|
|
def test_pod_start_no_deprecation(self, monkeypatch, capsys):
|
|
"""``pod start`` must not emit any deprecation warning."""
|
|
from winpodx.cli import pod as _pod
|
|
|
|
monkeypatch.setattr(
|
|
_pod,
|
|
"_start",
|
|
lambda wait, timeout, tuning_override=None: None,
|
|
)
|
|
|
|
ns = argparse.Namespace(pod_command="start", wait=False, timeout=300, tuning=None)
|
|
_pod.handle_pod(ns)
|
|
err = capsys.readouterr().err
|
|
assert "[deprecated]" not in err
|
|
|
|
@pytest.mark.parametrize("subcmd", ["stop", "status", "restart"])
|
|
def test_no_deprecation_for_basic_lifecycle(self, subcmd, monkeypatch, capsys):
|
|
from winpodx.cli import pod as _pod
|
|
|
|
monkeypatch.setattr(_pod, "_stop", lambda: None)
|
|
monkeypatch.setattr(_pod, "_status", lambda: None)
|
|
monkeypatch.setattr(_pod, "_restart", lambda: None)
|
|
|
|
ns = argparse.Namespace(pod_command=subcmd)
|
|
_pod.handle_pod(ns)
|
|
err = capsys.readouterr().err
|
|
assert "[deprecated]" not in err
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Part 5: pod alias commands — deprecation emitted, shared handler called
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestPodDeprecatedAliases:
|
|
"""Each moved pod subcommand must emit a deprecation line AND call the
|
|
shared handler function from pod.py."""
|
|
|
|
def _check_alias(
|
|
self,
|
|
monkeypatch,
|
|
capsys,
|
|
pod_subcmd: str,
|
|
new_full_cmd: str,
|
|
handler_name: str,
|
|
ns_extra: dict,
|
|
handler_kwargs: dict | None = None,
|
|
) -> None:
|
|
from winpodx.cli import pod as _pod
|
|
|
|
called = []
|
|
|
|
def _spy(*args, **kwargs):
|
|
called.append((args, kwargs))
|
|
|
|
monkeypatch.setattr(_pod, handler_name, _spy)
|
|
|
|
ns = argparse.Namespace(pod_command=pod_subcmd, **ns_extra)
|
|
_pod.handle_pod(ns)
|
|
|
|
err = capsys.readouterr().err
|
|
assert "[deprecated]" in err, f"No deprecation line for pod {pod_subcmd}"
|
|
assert f"pod {pod_subcmd}" in err
|
|
assert new_full_cmd in err
|
|
assert called, f"{handler_name} was not called for pod {pod_subcmd}"
|
|
|
|
def test_apply_fixes_deprecated(self, monkeypatch, capsys):
|
|
self._check_alias(
|
|
monkeypatch,
|
|
capsys,
|
|
"apply-fixes",
|
|
"guest apply-fixes",
|
|
"_apply_fixes",
|
|
{},
|
|
)
|
|
|
|
def test_sync_guest_deprecated(self, monkeypatch, capsys):
|
|
self._check_alias(
|
|
monkeypatch,
|
|
capsys,
|
|
"sync-guest",
|
|
"guest sync",
|
|
"_sync_guest",
|
|
{"force": False},
|
|
)
|
|
|
|
def test_sync_password_deprecated(self, monkeypatch, capsys):
|
|
self._check_alias(
|
|
monkeypatch,
|
|
capsys,
|
|
"sync-password",
|
|
"guest sync-password",
|
|
"_sync_password",
|
|
{"non_interactive": False},
|
|
)
|
|
|
|
def test_multi_session_deprecated(self, monkeypatch, capsys):
|
|
self._check_alias(
|
|
monkeypatch,
|
|
capsys,
|
|
"multi-session",
|
|
"guest multi-session",
|
|
"_multi_session",
|
|
{"action": "status"},
|
|
)
|
|
|
|
def test_recover_oem_deprecated(self, monkeypatch, capsys):
|
|
self._check_alias(
|
|
monkeypatch,
|
|
capsys,
|
|
"recover-oem",
|
|
"guest recover-oem",
|
|
"_recover_oem",
|
|
{},
|
|
)
|
|
|
|
def test_grow_disk_deprecated(self, monkeypatch, capsys):
|
|
self._check_alias(
|
|
monkeypatch,
|
|
capsys,
|
|
"grow-disk",
|
|
"install grow-disk",
|
|
"_grow_disk",
|
|
{"size": None, "increment": None, "extend_only": False, "yes": False},
|
|
)
|
|
|
|
def test_disk_usage_deprecated(self, monkeypatch, capsys):
|
|
self._check_alias(
|
|
monkeypatch,
|
|
capsys,
|
|
"disk-usage",
|
|
"install disk-usage",
|
|
"_disk_usage",
|
|
{},
|
|
)
|
|
|
|
def test_install_status_deprecated(self, monkeypatch, capsys):
|
|
"""pod install-status should emit deprecation and call handle_install_status."""
|
|
from winpodx.cli import pod as _pod
|
|
|
|
called = []
|
|
|
|
def _fake_install_status(args):
|
|
called.append(args)
|
|
return 0
|
|
|
|
monkeypatch.setattr("winpodx.cli.pod_install_status.handle", _fake_install_status)
|
|
|
|
ns = argparse.Namespace(
|
|
pod_command="install-status",
|
|
json=False,
|
|
no_color=False,
|
|
logs=False,
|
|
non_interactive=False,
|
|
)
|
|
with pytest.raises(SystemExit) as exc:
|
|
_pod.handle_pod(ns)
|
|
assert exc.value.code == 0
|
|
|
|
err = capsys.readouterr().err
|
|
assert "[deprecated]" in err
|
|
assert "pod install-status" in err
|
|
assert "install status" in err
|
|
assert called
|
|
|
|
def test_install_resume_deprecated(self, monkeypatch, capsys):
|
|
"""pod install-resume should emit deprecation and call handle_install_resume."""
|
|
from winpodx.cli import pod as _pod
|
|
|
|
called = []
|
|
|
|
def _fake_install_resume(args):
|
|
called.append(args)
|
|
return 0
|
|
|
|
monkeypatch.setattr("winpodx.cli.pod_install_resume.handle", _fake_install_resume)
|
|
|
|
ns = argparse.Namespace(
|
|
pod_command="install-resume",
|
|
non_interactive=False,
|
|
yes=False,
|
|
force=False,
|
|
)
|
|
with pytest.raises(SystemExit) as exc:
|
|
_pod.handle_pod(ns)
|
|
assert exc.value.code == 0
|
|
|
|
err = capsys.readouterr().err
|
|
assert "[deprecated]" in err
|
|
assert "pod install-resume" in err
|
|
assert "install resume" in err
|
|
assert called
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Part 6: new canonical guest commands dispatch to shared handlers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGuestDispatch:
|
|
def test_guest_apply_fixes_calls_handler(self, monkeypatch):
|
|
from winpodx.cli import guest as _guest
|
|
from winpodx.cli import pod as _pod
|
|
|
|
called = []
|
|
monkeypatch.setattr(_pod, "_apply_fixes", lambda: called.append(True))
|
|
|
|
ns = argparse.Namespace(guest_command="apply-fixes")
|
|
_guest.handle_guest(ns)
|
|
assert called
|
|
|
|
def test_guest_sync_calls_handler(self, monkeypatch):
|
|
from winpodx.cli import guest as _guest
|
|
from winpodx.cli import pod as _pod
|
|
|
|
called = []
|
|
monkeypatch.setattr(_pod, "_sync_guest", lambda force: called.append(force))
|
|
|
|
ns = argparse.Namespace(guest_command="sync", force=True)
|
|
_guest.handle_guest(ns)
|
|
assert called == [True]
|
|
|
|
def test_guest_sync_password_calls_handler(self, monkeypatch):
|
|
from winpodx.cli import guest as _guest
|
|
from winpodx.cli import pod as _pod
|
|
|
|
called = []
|
|
monkeypatch.setattr(
|
|
_pod, "_sync_password", lambda non_interactive: called.append(non_interactive)
|
|
)
|
|
|
|
ns = argparse.Namespace(guest_command="sync-password", non_interactive=False)
|
|
_guest.handle_guest(ns)
|
|
assert called == [False]
|
|
|
|
def test_guest_multi_session_calls_handler(self, monkeypatch):
|
|
from winpodx.cli import guest as _guest
|
|
from winpodx.cli import pod as _pod
|
|
|
|
called = []
|
|
monkeypatch.setattr(_pod, "_multi_session", lambda action: called.append(action))
|
|
|
|
ns = argparse.Namespace(guest_command="multi-session", action="status")
|
|
_guest.handle_guest(ns)
|
|
assert called == ["status"]
|
|
|
|
def test_guest_recover_oem_calls_handler(self, monkeypatch):
|
|
from winpodx.cli import guest as _guest
|
|
from winpodx.cli import pod as _pod
|
|
|
|
called = []
|
|
monkeypatch.setattr(_pod, "_recover_oem", lambda: called.append(True))
|
|
|
|
ns = argparse.Namespace(guest_command="recover-oem")
|
|
_guest.handle_guest(ns)
|
|
assert called
|
|
|
|
def test_guest_unknown_command_exits_one(self, capsys):
|
|
from winpodx.cli import guest as _guest
|
|
|
|
ns = argparse.Namespace(guest_command="nonexistent")
|
|
with pytest.raises(SystemExit) as exc:
|
|
_guest.handle_guest(ns)
|
|
assert exc.value.code == 1
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Part 7: new canonical install commands dispatch to shared handlers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestInstallDispatch:
|
|
def test_install_grow_disk_calls_handler(self, monkeypatch):
|
|
from winpodx.cli import install_cmd as _install
|
|
from winpodx.cli import pod as _pod
|
|
|
|
called = []
|
|
monkeypatch.setattr(
|
|
_pod,
|
|
"_grow_disk",
|
|
lambda target_size, increment, extend_only, assume_yes: called.append(
|
|
(target_size, increment, extend_only, assume_yes)
|
|
),
|
|
)
|
|
|
|
ns = argparse.Namespace(
|
|
install_command="grow-disk",
|
|
size=None,
|
|
increment=None,
|
|
extend_only=False,
|
|
yes=False,
|
|
)
|
|
_install.handle_install_group(ns)
|
|
assert called == [(None, None, False, False)]
|
|
|
|
def test_install_disk_usage_calls_handler(self, monkeypatch):
|
|
from winpodx.cli import install_cmd as _install
|
|
from winpodx.cli import pod as _pod
|
|
|
|
called = []
|
|
monkeypatch.setattr(_pod, "_disk_usage", lambda: called.append(True))
|
|
|
|
ns = argparse.Namespace(install_command="disk-usage")
|
|
_install.handle_install_group(ns)
|
|
assert called
|
|
|
|
def test_install_status_calls_handle(self, monkeypatch):
|
|
from winpodx.cli import install_cmd as _install
|
|
|
|
called = []
|
|
|
|
def _fake(args):
|
|
called.append(args)
|
|
return 0
|
|
|
|
monkeypatch.setattr("winpodx.cli.pod_install_status.handle", _fake)
|
|
|
|
ns = argparse.Namespace(
|
|
install_command="status",
|
|
json=False,
|
|
no_color=False,
|
|
logs=False,
|
|
non_interactive=False,
|
|
)
|
|
with pytest.raises(SystemExit) as exc:
|
|
_install.handle_install_group(ns)
|
|
assert exc.value.code == 0
|
|
assert called
|
|
|
|
def test_install_resume_calls_handle(self, monkeypatch):
|
|
from winpodx.cli import install_cmd as _install
|
|
|
|
called = []
|
|
|
|
def _fake(args):
|
|
called.append(args)
|
|
return 0
|
|
|
|
monkeypatch.setattr("winpodx.cli.pod_install_resume.handle", _fake)
|
|
|
|
ns = argparse.Namespace(
|
|
install_command="resume",
|
|
non_interactive=False,
|
|
yes=False,
|
|
force=False,
|
|
)
|
|
with pytest.raises(SystemExit) as exc:
|
|
_install.handle_install_group(ns)
|
|
assert exc.value.code == 0
|
|
assert called
|
|
|
|
def test_install_unknown_command_exits_one(self, capsys):
|
|
from winpodx.cli import install_cmd as _install
|
|
|
|
ns = argparse.Namespace(install_command="bogus")
|
|
with pytest.raises(SystemExit) as exc:
|
|
_install.handle_install_group(ns)
|
|
assert exc.value.code == 1
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Part 8: argparse parsing — new commands appear in the tree
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestArgparseParsing:
|
|
"""The CLI parser must accept the new command shapes without error."""
|
|
|
|
def test_guest_apply_fixes_parses(self):
|
|
ns = _parse(["guest", "apply-fixes"])
|
|
assert ns is not None
|
|
assert ns.command == "guest"
|
|
assert ns.guest_command == "apply-fixes"
|
|
|
|
def test_guest_sync_parses(self):
|
|
ns = _parse(["guest", "sync"])
|
|
assert ns is not None
|
|
assert ns.guest_command == "sync"
|
|
|
|
def test_guest_sync_force_flag(self):
|
|
ns = _parse(["guest", "sync", "--force"])
|
|
assert ns is not None
|
|
assert ns.force is True
|
|
|
|
def test_guest_sync_password_parses(self):
|
|
ns = _parse(["guest", "sync-password"])
|
|
assert ns is not None
|
|
assert ns.guest_command == "sync-password"
|
|
|
|
def test_guest_multi_session_parses(self):
|
|
ns = _parse(["guest", "multi-session", "on"])
|
|
assert ns is not None
|
|
assert ns.guest_command == "multi-session"
|
|
assert ns.action == "on"
|
|
|
|
def test_guest_recover_oem_parses(self):
|
|
ns = _parse(["guest", "recover-oem"])
|
|
assert ns is not None
|
|
assert ns.guest_command == "recover-oem"
|
|
|
|
def test_install_status_parses(self):
|
|
ns = _parse(["install", "status"])
|
|
assert ns is not None
|
|
assert ns.command == "install"
|
|
assert ns.install_command == "status"
|
|
|
|
def test_install_resume_parses(self):
|
|
ns = _parse(["install", "resume"])
|
|
assert ns is not None
|
|
assert ns.install_command == "resume"
|
|
|
|
def test_install_grow_disk_parses(self):
|
|
ns = _parse(["install", "grow-disk"])
|
|
assert ns is not None
|
|
assert ns.install_command == "grow-disk"
|
|
|
|
def test_install_grow_disk_size_arg(self):
|
|
ns = _parse(["install", "grow-disk", "128G"])
|
|
assert ns is not None
|
|
assert ns.size == "128G"
|
|
|
|
def test_install_disk_usage_parses(self):
|
|
ns = _parse(["install", "disk-usage"])
|
|
assert ns is not None
|
|
assert ns.install_command == "disk-usage"
|
|
|
|
def test_doctor_json_flag_parses(self):
|
|
ns = _parse(["doctor", "--json"])
|
|
assert ns is not None
|
|
assert ns.json is True
|
|
|
|
def test_doctor_quick_flag_parses(self):
|
|
ns = _parse(["doctor", "--quick"])
|
|
assert ns is not None
|
|
assert ns.quick is True
|
|
|
|
def test_doctor_both_flags(self):
|
|
ns = _parse(["doctor", "--json", "--quick"])
|
|
assert ns is not None
|
|
assert ns.json is True
|
|
assert ns.quick is True
|
|
|
|
# -- old pod aliases still parse --
|
|
|
|
def test_pod_apply_fixes_still_parses(self):
|
|
ns = _parse(["pod", "apply-fixes"])
|
|
assert ns is not None
|
|
assert ns.pod_command == "apply-fixes"
|
|
|
|
def test_pod_sync_guest_still_parses(self):
|
|
ns = _parse(["pod", "sync-guest"])
|
|
assert ns is not None
|
|
assert ns.pod_command == "sync-guest"
|
|
|
|
def test_pod_sync_password_still_parses(self):
|
|
ns = _parse(["pod", "sync-password"])
|
|
assert ns is not None
|
|
assert ns.pod_command == "sync-password"
|
|
|
|
def test_pod_multi_session_still_parses(self):
|
|
ns = _parse(["pod", "multi-session", "off"])
|
|
assert ns is not None
|
|
assert ns.pod_command == "multi-session"
|
|
|
|
def test_pod_recover_oem_still_parses(self):
|
|
ns = _parse(["pod", "recover-oem"])
|
|
assert ns is not None
|
|
assert ns.pod_command == "recover-oem"
|
|
|
|
def test_pod_install_status_still_parses(self):
|
|
ns = _parse(["pod", "install-status"])
|
|
assert ns is not None
|
|
assert ns.pod_command == "install-status"
|
|
|
|
def test_pod_install_resume_still_parses(self):
|
|
ns = _parse(["pod", "install-resume"])
|
|
assert ns is not None
|
|
assert ns.pod_command == "install-resume"
|
|
|
|
def test_pod_grow_disk_still_parses(self):
|
|
ns = _parse(["pod", "grow-disk"])
|
|
assert ns is not None
|
|
assert ns.pod_command == "grow-disk"
|
|
|
|
def test_pod_disk_usage_still_parses(self):
|
|
ns = _parse(["pod", "disk-usage"])
|
|
assert ns is not None
|
|
assert ns.pod_command == "disk-usage"
|
|
|
|
# -- info and check still parse (just deprecated) --
|
|
|
|
def test_info_still_parses(self):
|
|
ns = _parse(["info"])
|
|
assert ns is not None
|
|
assert ns.command == "info"
|
|
|
|
def test_check_still_parses(self):
|
|
ns = _parse(["check"])
|
|
assert ns is not None
|
|
assert ns.command == "check"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Part 9: ``winpodx --help`` lists expected top-level commands
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestTopLevelHelp:
|
|
def test_help_lists_pod(self):
|
|
stdout, _, _ = _run_cli(["--help"])
|
|
assert "pod" in stdout
|
|
|
|
def test_help_lists_guest(self):
|
|
stdout, _, _ = _run_cli(["--help"])
|
|
assert "guest" in stdout
|
|
|
|
def test_help_lists_install(self):
|
|
stdout, _, _ = _run_cli(["--help"])
|
|
assert "install" in stdout
|
|
|
|
def test_help_lists_doctor(self):
|
|
stdout, _, _ = _run_cli(["--help"])
|
|
assert "doctor" in stdout
|
|
|
|
def test_help_lists_provision(self):
|
|
stdout, _, _ = _run_cli(["--help"])
|
|
assert "provision" in stdout
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Part 10: grep install.sh / uninstall.sh for banned literals
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestShellScriptClean:
|
|
"""Assert no live invocations of deprecated/moved commands remain in the
|
|
shell scripts. Comment lines are excluded from the check."""
|
|
|
|
def _live_lines(self, path: Path) -> list[str]:
|
|
"""Return non-comment, non-blank lines from a shell script."""
|
|
if not path.exists():
|
|
return []
|
|
lines = []
|
|
for line in path.read_text(encoding="utf-8").splitlines():
|
|
stripped = line.strip()
|
|
if stripped and not stripped.startswith("#"):
|
|
lines.append(line)
|
|
return lines
|
|
|
|
@pytest.mark.parametrize(
|
|
"banned",
|
|
[
|
|
"winpodx pod sync-guest",
|
|
"winpodx pod apply-fixes",
|
|
"winpodx pod sync-password",
|
|
"winpodx pod multi-session",
|
|
"winpodx pod recover-oem",
|
|
"winpodx pod install-status",
|
|
"winpodx pod install-resume",
|
|
"winpodx pod grow-disk",
|
|
"winpodx pod disk-usage",
|
|
"winpodx info",
|
|
"winpodx check",
|
|
],
|
|
)
|
|
def test_install_sh_no_banned(self, banned):
|
|
path = _REPO / "install.sh"
|
|
live = self._live_lines(path)
|
|
hits = [line for line in live if banned in line]
|
|
assert hits == [], f"install.sh contains banned literal '{banned}': {hits}"
|
|
|
|
@pytest.mark.parametrize(
|
|
"banned",
|
|
[
|
|
"winpodx pod sync-guest",
|
|
"winpodx pod apply-fixes",
|
|
"winpodx info",
|
|
"winpodx check",
|
|
],
|
|
)
|
|
def test_uninstall_sh_no_banned(self, banned):
|
|
path = _REPO / "uninstall.sh"
|
|
live = self._live_lines(path)
|
|
hits = [line for line in live if banned in line]
|
|
assert hits == [], f"uninstall.sh contains banned literal '{banned}': {hits}"
|