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
556 lines
22 KiB
Python
556 lines
22 KiB
Python
# SPDX-License-Identifier: MIT
|
|
"""Tests for setup_cmd's half-uninstalled state guard.
|
|
|
|
Covers the case where ``winpodx setup --non-interactive`` is invoked
|
|
(by ``install.sh``) with an existing ``winpodx.toml`` but the podman/
|
|
docker container has been removed (e.g. by a non-purge ``uninstall.sh``
|
|
or external cleanup). Without this guard the next install.sh step
|
|
(``pod wait-ready``) would fail with ``Error: no such container ...``
|
|
and the user would have to do a ``--purge`` reinstall to recover.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import subprocess
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
def _make_existing_config(tmp_path):
|
|
"""Persist a podman config so handle_setup hits the existing-config branch."""
|
|
from winpodx.core.config import Config
|
|
|
|
cfg = Config()
|
|
cfg.pod.backend = "podman"
|
|
cfg.pod.container_name = "winpodx-windows"
|
|
cfg.rdp.user = "User"
|
|
cfg.rdp.password = "Pa55w0rd!Test" # noqa: S105 — fixture only
|
|
cfg.save()
|
|
return cfg
|
|
|
|
|
|
def _setup_args(non_interactive: bool = True) -> argparse.Namespace:
|
|
return argparse.Namespace(
|
|
backend=None,
|
|
non_interactive=non_interactive,
|
|
update_image=False,
|
|
migrate_storage=False,
|
|
)
|
|
|
|
|
|
def _mock_freerdp_ok():
|
|
dep = MagicMock()
|
|
dep.found = True
|
|
dep.note = ""
|
|
return {"freerdp": dep}
|
|
|
|
|
|
class TestContainerExistsOnBackend:
|
|
def test_returns_true_when_container_in_ps(self, tmp_path, monkeypatch):
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
cfg = _make_existing_config(tmp_path)
|
|
|
|
from winpodx.cli.setup_cmd import _container_exists_on_backend
|
|
|
|
completed = subprocess.CompletedProcess(
|
|
args=["podman", "ps", "-a", "--format", "{{.Names}}"],
|
|
returncode=0,
|
|
stdout="winpodx-windows\nother-container\n",
|
|
stderr="",
|
|
)
|
|
with patch("subprocess.run", return_value=completed):
|
|
assert _container_exists_on_backend(cfg) is True
|
|
|
|
def test_returns_false_when_container_absent(self, tmp_path, monkeypatch):
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
cfg = _make_existing_config(tmp_path)
|
|
|
|
from winpodx.cli.setup_cmd import _container_exists_on_backend
|
|
|
|
completed = subprocess.CompletedProcess(
|
|
args=["podman", "ps", "-a", "--format", "{{.Names}}"],
|
|
returncode=0,
|
|
stdout="other-container\n",
|
|
stderr="",
|
|
)
|
|
with patch("subprocess.run", return_value=completed):
|
|
assert _container_exists_on_backend(cfg) is False
|
|
|
|
def test_returns_false_on_subprocess_error(self, tmp_path, monkeypatch):
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
cfg = _make_existing_config(tmp_path)
|
|
|
|
from winpodx.cli.setup_cmd import _container_exists_on_backend
|
|
|
|
with patch("subprocess.run", side_effect=OSError("podman not found")):
|
|
assert _container_exists_on_backend(cfg) is False
|
|
|
|
def test_returns_false_on_timeout(self, tmp_path, monkeypatch):
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
cfg = _make_existing_config(tmp_path)
|
|
|
|
from winpodx.cli.setup_cmd import _container_exists_on_backend
|
|
|
|
with patch(
|
|
"subprocess.run",
|
|
side_effect=subprocess.TimeoutExpired(cmd="podman", timeout=10),
|
|
):
|
|
assert _container_exists_on_backend(cfg) is False
|
|
|
|
def test_returns_false_when_backend_not_container(self, tmp_path, monkeypatch):
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
cfg = _make_existing_config(tmp_path)
|
|
cfg.pod.backend = "manual"
|
|
|
|
from winpodx.cli.setup_cmd import _container_exists_on_backend
|
|
|
|
# No subprocess.run call at all for non-container backends.
|
|
with patch("subprocess.run", side_effect=AssertionError("must not be called")):
|
|
assert _container_exists_on_backend(cfg) is False
|
|
|
|
|
|
class TestHalfUninstalledGuard:
|
|
def test_skips_setup_when_container_exists(self, tmp_path, monkeypatch):
|
|
"""Happy path: config + container both present → setup still skipped,
|
|
ensure_ready NOT called (preserves pre-fix behaviour)."""
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
monkeypatch.setattr("sys.stdin", MagicMock(isatty=lambda: False))
|
|
_make_existing_config(tmp_path)
|
|
|
|
ensure_ready_mock = MagicMock()
|
|
with (
|
|
patch("winpodx.cli.setup_cmd.check_all", return_value=_mock_freerdp_ok()),
|
|
patch("winpodx.cli.setup_cmd.import_winapps_config", return_value=None),
|
|
patch("winpodx.cli.setup_cmd._ensure_oem_token_staged"),
|
|
patch(
|
|
"winpodx.cli.setup_cmd._container_exists_on_backend",
|
|
return_value=True,
|
|
),
|
|
patch("winpodx.core.provisioner.ensure_ready", ensure_ready_mock),
|
|
):
|
|
from winpodx.cli.setup_cmd import handle_setup
|
|
|
|
handle_setup(_setup_args(non_interactive=True))
|
|
|
|
assert ensure_ready_mock.call_count == 0, (
|
|
"ensure_ready must not run when container is healthy"
|
|
)
|
|
|
|
def test_marks_initialized_on_existing_config_skip(self, tmp_path, monkeypatch):
|
|
"""#341: non-interactive setup over an existing config whose
|
|
`initialized` flag is still False must flip it True (and persist),
|
|
so the first-run prompt stops firing on every invocation."""
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
monkeypatch.setattr("sys.stdin", MagicMock(isatty=lambda: False))
|
|
cfg = _make_existing_config(tmp_path)
|
|
assert cfg.pod.initialized is False # precondition: the buggy state
|
|
|
|
with (
|
|
patch("winpodx.cli.setup_cmd.check_all", return_value=_mock_freerdp_ok()),
|
|
patch("winpodx.cli.setup_cmd.import_winapps_config", return_value=None),
|
|
patch("winpodx.cli.setup_cmd._ensure_oem_token_staged"),
|
|
patch(
|
|
"winpodx.cli.setup_cmd._container_exists_on_backend",
|
|
return_value=True,
|
|
),
|
|
):
|
|
from winpodx.cli.setup_cmd import handle_setup
|
|
|
|
handle_setup(_setup_args(non_interactive=True))
|
|
|
|
from winpodx.core.config import Config
|
|
|
|
assert Config.load().pod.initialized is True, (
|
|
"existing-config skip path must mark the install initialized (#341)"
|
|
)
|
|
|
|
def test_freerdp_source_persisted_on_existing_config(self, tmp_path, monkeypatch):
|
|
"""`winpodx setup --freerdp-source flatpak` on an existing config must
|
|
persist the preference even on the skip path."""
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
monkeypatch.setattr("sys.stdin", MagicMock(isatty=lambda: False))
|
|
_make_existing_config(tmp_path)
|
|
|
|
args = _setup_args(non_interactive=True)
|
|
args.freerdp_source = "flatpak"
|
|
|
|
with (
|
|
patch("winpodx.cli.setup_cmd.check_all", return_value=_mock_freerdp_ok()),
|
|
patch("winpodx.cli.setup_cmd.import_winapps_config", return_value=None),
|
|
patch("winpodx.cli.setup_cmd._ensure_oem_token_staged"),
|
|
patch(
|
|
"winpodx.cli.setup_cmd._container_exists_on_backend",
|
|
return_value=True,
|
|
),
|
|
):
|
|
from winpodx.cli.setup_cmd import handle_setup
|
|
|
|
handle_setup(args)
|
|
|
|
from winpodx.core.config import Config
|
|
|
|
assert Config.load().rdp.freerdp_source == "flatpak"
|
|
|
|
def test_calls_ensure_ready_when_container_missing(self, tmp_path, monkeypatch):
|
|
"""Half-uninstalled: config present, container gone → ensure_ready
|
|
is called to recreate the container from compose.yaml."""
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
monkeypatch.setattr("sys.stdin", MagicMock(isatty=lambda: False))
|
|
_make_existing_config(tmp_path)
|
|
|
|
from winpodx.core.pod import PodState, PodStatus
|
|
|
|
ensure_ready_mock = MagicMock()
|
|
with (
|
|
patch("winpodx.cli.setup_cmd.check_all", return_value=_mock_freerdp_ok()),
|
|
patch("winpodx.cli.setup_cmd.import_winapps_config", return_value=None),
|
|
patch("winpodx.cli.setup_cmd._ensure_oem_token_staged"),
|
|
patch(
|
|
"winpodx.core.pod.pod_status",
|
|
return_value=PodStatus(state=PodState.STOPPED),
|
|
),
|
|
patch(
|
|
"winpodx.cli.setup_cmd._container_exists_on_backend",
|
|
return_value=False,
|
|
),
|
|
patch("winpodx.core.provisioner.ensure_ready", ensure_ready_mock),
|
|
):
|
|
from winpodx.cli.setup_cmd import handle_setup
|
|
|
|
handle_setup(_setup_args(non_interactive=True))
|
|
|
|
assert ensure_ready_mock.call_count == 1, "ensure_ready must run when container is missing"
|
|
|
|
def test_does_not_crash_when_ensure_ready_raises(self, tmp_path, monkeypatch, capsys):
|
|
"""Recovery is best-effort: if ensure_ready can't bring the pod up,
|
|
print a warning + recovery hint and return cleanly so install.sh's
|
|
downstream steps still get a chance to run."""
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
monkeypatch.setattr("sys.stdin", MagicMock(isatty=lambda: False))
|
|
_make_existing_config(tmp_path)
|
|
|
|
from winpodx.core.pod import PodState, PodStatus
|
|
|
|
with (
|
|
patch("winpodx.cli.setup_cmd.check_all", return_value=_mock_freerdp_ok()),
|
|
patch("winpodx.cli.setup_cmd.import_winapps_config", return_value=None),
|
|
patch("winpodx.cli.setup_cmd._ensure_oem_token_staged"),
|
|
patch(
|
|
"winpodx.core.pod.pod_status",
|
|
return_value=PodStatus(state=PodState.STOPPED),
|
|
),
|
|
patch(
|
|
"winpodx.cli.setup_cmd._container_exists_on_backend",
|
|
return_value=False,
|
|
),
|
|
patch(
|
|
"winpodx.core.provisioner.ensure_ready",
|
|
side_effect=RuntimeError("podman socket down"),
|
|
),
|
|
):
|
|
from winpodx.cli.setup_cmd import handle_setup
|
|
|
|
# Must not raise.
|
|
handle_setup(_setup_args(non_interactive=True))
|
|
|
|
captured = capsys.readouterr().out
|
|
assert "WARNING: could not start pod" in captured
|
|
assert "podman socket down" in captured
|
|
assert "uninstall.sh --purge" in captured
|
|
|
|
def test_does_not_run_for_manual_backend(self, tmp_path, monkeypatch):
|
|
"""The manual backend doesn't have a 'container' to detect; the heal
|
|
path must short-circuit before probing pod_status so the function is a
|
|
no-op for non-container deployments. (libvirt was dropped in 0.6.0.)"""
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
monkeypatch.setattr("sys.stdin", MagicMock(isatty=lambda: False))
|
|
cfg = _make_existing_config(tmp_path)
|
|
cfg.pod.backend = "manual"
|
|
cfg.save()
|
|
|
|
ensure_ready_mock = MagicMock()
|
|
container_check_mock = MagicMock(return_value=False)
|
|
with (
|
|
patch("winpodx.cli.setup_cmd.check_all", return_value=_mock_freerdp_ok()),
|
|
patch("winpodx.cli.setup_cmd.import_winapps_config", return_value=None),
|
|
patch("winpodx.cli.setup_cmd._ensure_oem_token_staged"),
|
|
patch(
|
|
"winpodx.cli.setup_cmd._container_exists_on_backend",
|
|
container_check_mock,
|
|
),
|
|
patch("winpodx.core.provisioner.ensure_ready", ensure_ready_mock),
|
|
):
|
|
from winpodx.cli.setup_cmd import handle_setup
|
|
|
|
handle_setup(_setup_args(non_interactive=True))
|
|
|
|
assert container_check_mock.call_count == 0
|
|
assert ensure_ready_mock.call_count == 0
|
|
|
|
|
|
class TestHealHelperDirect:
|
|
"""Direct unit tests for _heal_missing_container_if_needed."""
|
|
|
|
def test_pod_status_failure_treated_as_missing(self, tmp_path, monkeypatch):
|
|
"""If pod_status raises (e.g. podman not on PATH at probe time)
|
|
AND _container_exists_on_backend says False, we still attempt the
|
|
heal — better to try and fail loud than silently skip."""
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
cfg = _make_existing_config(tmp_path)
|
|
|
|
ensure_ready_mock = MagicMock()
|
|
with (
|
|
patch(
|
|
"winpodx.core.pod.pod_status",
|
|
side_effect=RuntimeError("backend init failed"),
|
|
),
|
|
patch(
|
|
"winpodx.cli.setup_cmd._container_exists_on_backend",
|
|
return_value=False,
|
|
),
|
|
patch("winpodx.core.provisioner.ensure_ready", ensure_ready_mock),
|
|
):
|
|
from winpodx.cli.setup_cmd import _heal_missing_container_if_needed
|
|
|
|
_heal_missing_container_if_needed(cfg)
|
|
|
|
assert ensure_ready_mock.call_count == 1
|
|
|
|
@pytest.mark.parametrize("state_name", ["RUNNING", "PAUSED", "STARTING"])
|
|
def test_skips_when_pod_already_alive(self, tmp_path, monkeypatch, state_name):
|
|
"""If pod_status reports the pod is alive in any form, the heal
|
|
path must not try to recreate it — that would needlessly cycle a
|
|
working container."""
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
cfg = _make_existing_config(tmp_path)
|
|
|
|
from winpodx.core.pod import PodState, PodStatus
|
|
|
|
ensure_ready_mock = MagicMock()
|
|
with (
|
|
patch(
|
|
"winpodx.core.pod.pod_status",
|
|
return_value=PodStatus(state=getattr(PodState, state_name)),
|
|
),
|
|
patch("winpodx.core.provisioner.ensure_ready", ensure_ready_mock),
|
|
):
|
|
from winpodx.cli.setup_cmd import _heal_missing_container_if_needed
|
|
|
|
_heal_missing_container_if_needed(cfg)
|
|
|
|
assert ensure_ready_mock.call_count == 0
|
|
|
|
|
|
class TestResolveCredentials:
|
|
"""`_resolve_credentials` decides whether to prompt, generate, or preserve.
|
|
|
|
Regression coverage for #216: an interactive `winpodx setup` rerun on an
|
|
existing install used to reprompt for the password and silently
|
|
overwrite cfg.rdp.password, which desynced from the Windows guest
|
|
account (dockur honors USERNAME/PASSWORD only on first boot) and locked
|
|
the user out at next launch.
|
|
"""
|
|
|
|
def test_non_interactive_generates_fresh_credentials(self, tmp_path, monkeypatch):
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
from winpodx.cli.setup_cmd import _resolve_credentials
|
|
from winpodx.core.config import Config
|
|
|
|
cfg = Config()
|
|
cfg.rdp.user = ""
|
|
cfg.rdp.password = ""
|
|
|
|
_resolve_credentials(cfg, non_interactive=True, config_existed=False)
|
|
|
|
assert cfg.rdp.user == "WPX-User"
|
|
assert cfg.rdp.password # randomly generated
|
|
assert cfg.rdp.ip == "127.0.0.1"
|
|
assert cfg.rdp.password_updated
|
|
|
|
def test_preserves_password_when_config_existed(self, tmp_path, monkeypatch):
|
|
"""Interactive rerun on an existing install must not reprompt for
|
|
the password — that would desync the host config from the Windows
|
|
guest account. #216."""
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
from winpodx.cli.setup_cmd import _resolve_credentials
|
|
from winpodx.core.config import Config
|
|
|
|
cfg = Config()
|
|
cfg.pod.backend = "podman"
|
|
cfg.rdp.user = "User"
|
|
cfg.rdp.password = "OldPa55w0rd!" # noqa: S105 — fixture only
|
|
cfg.rdp.password_updated = "2026-01-01T00:00:00+00:00"
|
|
cfg.rdp.ip = "127.0.0.1"
|
|
|
|
getpass_mock = MagicMock(return_value="NewPa55w0rd!")
|
|
ask_mock = MagicMock(return_value="bogus")
|
|
with (
|
|
patch("winpodx.cli.setup_cmd._ask", ask_mock),
|
|
patch("getpass.getpass", getpass_mock),
|
|
):
|
|
_resolve_credentials(cfg, non_interactive=False, config_existed=True)
|
|
|
|
assert cfg.rdp.user == "User"
|
|
assert cfg.rdp.password == "OldPa55w0rd!"
|
|
assert cfg.rdp.password_updated == "2026-01-01T00:00:00+00:00"
|
|
assert cfg.rdp.ip == "127.0.0.1"
|
|
getpass_mock.assert_not_called()
|
|
ask_mock.assert_not_called()
|
|
|
|
def test_interactive_fresh_install_prompts_for_credentials(self, tmp_path, monkeypatch):
|
|
"""No prior config — the wizard must ask for user / password / ip."""
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
from winpodx.cli.setup_cmd import _resolve_credentials
|
|
from winpodx.core.config import Config
|
|
|
|
cfg = Config()
|
|
cfg.pod.backend = "podman"
|
|
|
|
ask_returns = iter(["alice", "127.0.0.1"])
|
|
ask_mock = MagicMock(side_effect=lambda *a, **kw: next(ask_returns))
|
|
getpass_mock = MagicMock(return_value="ChosenPassw0rd!")
|
|
|
|
with (
|
|
patch("winpodx.cli.setup_cmd._ask", ask_mock),
|
|
patch("getpass.getpass", getpass_mock),
|
|
):
|
|
_resolve_credentials(cfg, non_interactive=False, config_existed=False)
|
|
|
|
assert cfg.rdp.user == "alice"
|
|
assert cfg.rdp.password == "ChosenPassw0rd!"
|
|
assert cfg.rdp.ip == "127.0.0.1"
|
|
getpass_mock.assert_called_once()
|
|
|
|
def test_interactive_existing_config_but_blank_password_still_prompts(
|
|
self, tmp_path, monkeypatch
|
|
):
|
|
"""An existing config with an empty password (mid-init / corruption)
|
|
should not silently keep the blank — fall back to the prompt."""
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
from winpodx.cli.setup_cmd import _resolve_credentials
|
|
from winpodx.core.config import Config
|
|
|
|
cfg = Config()
|
|
cfg.pod.backend = "podman"
|
|
cfg.rdp.user = "User"
|
|
cfg.rdp.password = ""
|
|
|
|
ask_returns = iter(["User", "127.0.0.1"])
|
|
ask_mock = MagicMock(side_effect=lambda *a, **kw: next(ask_returns))
|
|
getpass_mock = MagicMock(return_value="FillIn!")
|
|
|
|
with (
|
|
patch("winpodx.cli.setup_cmd._ask", ask_mock),
|
|
patch("getpass.getpass", getpass_mock),
|
|
):
|
|
_resolve_credentials(cfg, non_interactive=False, config_existed=True)
|
|
|
|
assert cfg.rdp.password == "FillIn!"
|
|
getpass_mock.assert_called_once()
|
|
|
|
|
|
class TestDecideStorageModeExplicitTarget:
|
|
"""`--storage-path` / install.sh `--storage-dir` (#646)."""
|
|
|
|
def test_explicit_target_fresh_sets_storage_path(self, tmp_path):
|
|
from pathlib import Path
|
|
|
|
from winpodx.cli.setup_cmd import _decide_storage_mode
|
|
from winpodx.core.config import Config
|
|
|
|
target = tmp_path / "roomy" / "winpodx"
|
|
cfg = Config()
|
|
cfg.pod.backend = "podman"
|
|
cfg.pod.storage_path = ""
|
|
with (
|
|
patch("winpodx.core.storage_migration.resolve_named_volume", return_value=None),
|
|
patch("winpodx.utils.btrfs.detect_path_fs", return_value="ext4"),
|
|
patch("winpodx.utils.btrfs.host_storage_is_ssd", return_value=False),
|
|
):
|
|
_decide_storage_mode(cfg, non_interactive=True, explicit_target=Path(target))
|
|
assert cfg.pod.storage_path == str(target)
|
|
assert target.is_dir()
|
|
|
|
def test_explicit_target_ignored_when_already_configured(self, tmp_path):
|
|
from pathlib import Path
|
|
|
|
from winpodx.cli.setup_cmd import _decide_storage_mode
|
|
from winpodx.core.config import Config
|
|
|
|
cfg = Config()
|
|
cfg.pod.backend = "podman"
|
|
cfg.pod.storage_path = "/existing/storage"
|
|
_decide_storage_mode(cfg, non_interactive=True, explicit_target=Path(tmp_path / "other"))
|
|
# An already-configured install isn't relocated here (that's --migrate-storage).
|
|
assert cfg.pod.storage_path == "/existing/storage"
|
|
|
|
|
|
class TestStageWinIso:
|
|
"""`_stage_win_iso` — #647: stage <storage>/custom.iso before compose-up."""
|
|
|
|
def _iso(self, tmp_path):
|
|
from pathlib import Path
|
|
|
|
p = Path(tmp_path) / "win.iso"
|
|
p.write_bytes(b"0" * 4096)
|
|
return p
|
|
|
|
def test_none_path_is_noop(self, tmp_path):
|
|
from winpodx.cli.setup_cmd import _stage_win_iso
|
|
from winpodx.core.config import Config
|
|
|
|
cfg = Config()
|
|
cfg.pod.storage_path = str(tmp_path / "storage")
|
|
_stage_win_iso(cfg, None)
|
|
assert not (tmp_path / "storage" / "custom.iso").exists()
|
|
|
|
def test_stages_to_custom_iso(self, tmp_path):
|
|
from pathlib import Path
|
|
|
|
from winpodx.cli.setup_cmd import _stage_win_iso
|
|
from winpodx.core.config import Config
|
|
|
|
iso = self._iso(tmp_path)
|
|
cfg = Config()
|
|
cfg.pod.storage_path = str(tmp_path / "storage")
|
|
_stage_win_iso(cfg, str(iso))
|
|
dst = Path(tmp_path) / "storage" / "custom.iso"
|
|
assert dst.is_file()
|
|
assert dst.read_bytes() == iso.read_bytes()
|
|
|
|
def test_no_storage_path_skips(self, tmp_path):
|
|
from winpodx.cli.setup_cmd import _stage_win_iso
|
|
from winpodx.core.config import Config
|
|
|
|
iso = self._iso(tmp_path)
|
|
cfg = Config()
|
|
cfg.pod.storage_path = "" # legacy named-volume → no host dir
|
|
_stage_win_iso(cfg, str(iso)) # must not raise
|
|
# nothing to assert beyond "no crash"; named-volume has no host dir
|
|
|
|
def test_missing_iso_skips(self, tmp_path):
|
|
from winpodx.cli.setup_cmd import _stage_win_iso
|
|
from winpodx.core.config import Config
|
|
|
|
cfg = Config()
|
|
cfg.pod.storage_path = str(tmp_path / "storage")
|
|
_stage_win_iso(cfg, str(tmp_path / "nope.iso")) # must not raise
|
|
assert not (tmp_path / "storage" / "custom.iso").exists()
|
|
|
|
def test_same_file_is_noop(self, tmp_path):
|
|
from pathlib import Path
|
|
|
|
from winpodx.cli.setup_cmd import _stage_win_iso
|
|
from winpodx.core.config import Config
|
|
|
|
storage = Path(tmp_path) / "storage"
|
|
storage.mkdir()
|
|
dst = storage / "custom.iso"
|
|
dst.write_bytes(b"0" * 4096)
|
|
cfg = Config()
|
|
cfg.pod.storage_path = str(storage)
|
|
_stage_win_iso(cfg, str(dst)) # src == dst → no error, no truncation
|
|
assert dst.read_bytes() == b"0" * 4096
|