"""Tests for ``template deploy-win`` (Windows Server 2025, Linux host).""" from __future__ import annotations import subprocess import types from pathlib import Path from typing import Any import pytest from click.testing import CliRunner import ci_orchestrator.commands.template as tmpl_mod from ci_orchestrator.__main__ import cli from ci_orchestrator.winimage import uup def _make_config(root: Path) -> types.SimpleNamespace: paths = types.SimpleNamespace( root=root, templates=root / "templates", build_vms=root / "build-vms", artifacts=root / "artifacts", keys=root / "keys", ) return types.SimpleNamespace(vmrun_path=None, paths=paths, ssh_key_path=None) # ── helper unit tests ─────────────────────────────────────────────────────── def test_winbuild_vmx_lines() -> None: lines = tmpl_mod._winbuild_vmx_lines( vm_name="WinBuild2025", mem_mb=6144, vcpu_count=4, cores_per_socket=2, vmdk_name="WinBuild2025.vmdk", install_iso="/iso/in.iso", tools_iso="/iso/tools.iso", ) text = "\n".join(lines) assert 'guestOS = "windows2019srvnext-64"' in text assert 'firmware = "efi"' in text assert 'sata0:0.fileName = "/iso/in.iso"' in text assert 'sata0:1.fileName = "/iso/tools.iso"' in text assert 'ethernet0.virtualDev = "e1000e"' in text assert 'nvme0:0.fileName = "WinBuild2025.vmdk"' in text def test_remove_vmx_lines(tmp_path: Path) -> None: vmx = tmp_path / "x.vmx" vmx.write_text( 'nvme0:0.present = "TRUE"\n' 'sata0:0.present = "TRUE"\n' 'sata0:1.fileName = "t.iso"\n' 'usb.present = "TRUE"\n' 'ethernet0.virtualDev = "e1000e"\n', encoding="ascii", ) assert tmpl_mod._remove_vmx_lines(vmx, "sata0") == 2 assert tmpl_mod._remove_vmx_lines(vmx, "usb") == 1 remaining = vmx.read_text(encoding="ascii") assert "sata0" not in remaining and "usb" not in remaining assert "nvme0:0" in remaining and "ethernet0" in remaining def test_wait_install_flag_ok(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(tmpl_mod.time, "sleep", lambda _s: None) def fake_run(argv: list[str], **_kw: Any) -> subprocess.CompletedProcess[str]: if "checkToolsState" in argv: return subprocess.CompletedProcess(argv, 0, "running\n", "") if "fileExistsInGuest" in argv: return subprocess.CompletedProcess(argv, 0, "The file exists.\n", "") return subprocess.CompletedProcess(argv, 0, "", "") monkeypatch.setattr(tmpl_mod.subprocess, "run", fake_run) assert tmpl_mod._wait_install_flag( "vmrun", "x.vmx", admin_user="a", admin_password="p", flag_path="C:\\f.flag", timeout_s=10.0, poll_s=0.0, ) is True def test_wait_install_flag_timeout(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(tmpl_mod.time, "sleep", lambda _s: None) monkeypatch.setattr( tmpl_mod.subprocess, "run", lambda argv, **_k: subprocess.CompletedProcess(argv, 0, "not-running", ""), ) assert tmpl_mod._wait_install_flag( "vmrun", "x.vmx", admin_user="a", admin_password="p", flag_path="C:\\f.flag", timeout_s=-1.0, poll_s=0.0, ) is False # ── CLI flow ──────────────────────────────────────────────────────────────── def _patch_deploy( monkeypatch: pytest.MonkeyPatch, root: Path, *, snapshot: str = "PostInstall" ) -> dict[str, Any]: """Stub all external effects; return a dict capturing key calls.""" seen: dict[str, Any] = {"patched": False, "vmdk": False} vdisk = root / "vdiskmgr" vdisk.write_bytes(b"") monkeypatch.setattr(tmpl_mod, "load_config", lambda: _make_config(root)) monkeypatch.setattr(tmpl_mod, "_find_vdisk_manager", lambda _e: str(vdisk)) monkeypatch.setattr(tmpl_mod.shutil, "which", lambda name: f"/usr/bin/{name}") monkeypatch.setattr(tmpl_mod.time, "sleep", lambda _s: None) monkeypatch.setattr(tmpl_mod, "_wait_install_flag", lambda *a, **k: True) monkeypatch.setattr(tmpl_mod, "_poll_guest_ip_deploy", lambda *a, **k: "192.168.79.50") monkeypatch.setattr(tmpl_mod, "_vmrun_soft_stop", lambda *a, **k: None) def fake_patch_iso(base: Path, *, out_iso: Path, **_k: Any) -> Path: out_iso.write_bytes(b"iso") seen["patched"] = True return out_iso monkeypatch.setattr(tmpl_mod.win_iso_patch, "patch_iso", fake_patch_iso) def fake_run(argv: list[str], **_kw: Any) -> subprocess.CompletedProcess[str]: if "listSnapshots" in argv: return subprocess.CompletedProcess(argv, 0, f"Total: 1\n{snapshot}\n", "") if "vdiskmgr" in str(argv[0]): seen["vmdk"] = True return subprocess.CompletedProcess(argv, 0, "", "") monkeypatch.setattr(tmpl_mod.subprocess, "run", fake_run) monkeypatch.setattr(tmpl_mod.subprocess, "Popen", lambda *a, **k: types.SimpleNamespace()) return seen def test_deploy_win_winiso_happy(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path (root / "iso").mkdir(parents=True) tools = root / "iso" / "VMware-tools-windows.iso" tools.write_bytes(b"tools") base = root / "iso" / "ws.iso" base.write_bytes(b"win") seen = _patch_deploy(monkeypatch, root) result = CliRunner().invoke( cli, ["template", "deploy-win", "--win-iso", str(base), "--nogui"], ) assert result.exit_code == 0, result.output assert seen["patched"] and seen["vmdk"] assert "deploy-win complete" in result.output vmx = root / "templates" / "WinBuild2025" / "WinBuild2025.vmx" assert vmx.is_file() text = vmx.read_text(encoding="ascii") # Step 9 stripped install media + swapped NIC assert "sata0" not in text assert 'ethernet0.virtualDev = "vmxnet3"' in text def test_deploy_win_uup_happy(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path (root / "iso").mkdir(parents=True) (root / "iso" / "VMware-tools-windows.iso").write_bytes(b"tools") _patch_deploy(monkeypatch, root) monkeypatch.setattr(tmpl_mod.win_uup, "check_tools", lambda: []) build = uup.UupBuild(uuid="u", build="26100.32860", title="t", arch="amd64") monkeypatch.setattr(tmpl_mod.win_uup, "resolve_build", lambda **_k: build) def fake_build_iso(_b: uup.UupBuild, *, dest_dir: Path, **_k: Any) -> Path: p = dest_dir / "ws2025-26100.32860.iso" p.write_bytes(b"iso") return p monkeypatch.setattr(tmpl_mod.win_uup, "build_iso", fake_build_iso) result = CliRunner().invoke(cli, ["template", "deploy-win", "--nogui"]) assert result.exit_code == 0, result.output assert "26100.32860" in result.output def test_deploy_win_bad_computer_name(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path (root / "iso").mkdir(parents=True) (root / "iso" / "VMware-tools-windows.iso").write_bytes(b"t") base = root / "iso" / "ws.iso" base.write_bytes(b"w") _patch_deploy(monkeypatch, root) result = CliRunner().invoke( cli, ["template", "deploy-win", "--win-iso", str(base), "--computer-name", "bad name!"], ) assert result.exit_code != 0 assert "invalid computer name" in result.output def test_deploy_win_missing_tools_iso(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path (root / "iso").mkdir(parents=True) base = root / "iso" / "ws.iso" base.write_bytes(b"w") _patch_deploy(monkeypatch, root) # tools iso intentionally absent result = CliRunner().invoke( cli, ["template", "deploy-win", "--win-iso", str(base)] ) assert result.exit_code != 0 assert "VMware Tools ISO" in result.output def test_deploy_win_flag_timeout(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path (root / "iso").mkdir(parents=True) (root / "iso" / "VMware-tools-windows.iso").write_bytes(b"t") base = root / "iso" / "ws.iso" base.write_bytes(b"w") _patch_deploy(monkeypatch, root) monkeypatch.setattr(tmpl_mod, "_wait_install_flag", lambda *a, **k: False) result = CliRunner().invoke( cli, ["template", "deploy-win", "--win-iso", str(base), "--nogui"] ) assert result.exit_code != 0 assert "install_complete.flag not seen" in result.output