b2b31f4b6e
Bring monitor.py 49->99%%, vm.py 79->98%%, winrm.py 76->100%%. Total project coverage 80.10%% -> 90.69%%, satisfying the 90%% gate set in commit 451a61c.
295 lines
8.7 KiB
Python
295 lines
8.7 KiB
Python
"""Tests for ``ci_orchestrator vm new``.
|
|
|
|
Migrated from Pester ``tests/New-BuildVM.Tests.ps1``. Preserves:
|
|
|
|
* parameter validation: missing template VMX rejected
|
|
* failure path: backend clone failure cleans up the partial clone dir
|
|
* clone naming: ``Clone_{JobId}_{yyyyMMdd_HHmmss}``
|
|
* output: stdout final line is the clone VMX path
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
import ci_orchestrator.commands.vm as vm_module
|
|
from ci_orchestrator.__main__ import cli
|
|
from ci_orchestrator.backends.errors import BackendOperationFailed
|
|
from ci_orchestrator.backends.protocol import VmHandle
|
|
|
|
|
|
class _FakeBackend:
|
|
"""Records calls and creates the destination VMX on success."""
|
|
|
|
def __init__(self, *, fail: bool = False) -> None:
|
|
self.fail = fail
|
|
self.calls: list[dict[str, Any]] = []
|
|
|
|
def clone_linked(
|
|
self,
|
|
template: str,
|
|
snapshot: str,
|
|
name: str,
|
|
destination: str | None = None,
|
|
) -> VmHandle:
|
|
self.calls.append(
|
|
{
|
|
"template": template,
|
|
"snapshot": snapshot,
|
|
"name": name,
|
|
"destination": destination,
|
|
}
|
|
)
|
|
if self.fail:
|
|
raise BackendOperationFailed("clone", 1, "Error: source not found")
|
|
# Real vmrun creates dest dir + VMX file. Mirror that here so the
|
|
# post-clone existence check passes.
|
|
assert destination is not None
|
|
dst = Path(destination)
|
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
dst.write_text("config.version = \"8\"", encoding="utf-8")
|
|
return VmHandle(identifier=str(dst), name=name)
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_template(tmp_path: Path) -> Path:
|
|
p = tmp_path / "WinBuild2025.vmx"
|
|
p.write_text("config.version = \"8\"", encoding="utf-8")
|
|
return p
|
|
|
|
|
|
def test_vm_new_rejects_missing_template(tmp_path: Path) -> None:
|
|
"""Pester migration: throws when TemplatePath does not exist."""
|
|
missing = tmp_path / "ghost.vmx"
|
|
base = tmp_path / "build-vms"
|
|
result = CliRunner().invoke(
|
|
cli,
|
|
[
|
|
"vm",
|
|
"new",
|
|
"--template",
|
|
str(missing),
|
|
"--clone-base-dir",
|
|
str(base),
|
|
"--job-id",
|
|
"test-1",
|
|
],
|
|
)
|
|
assert result.exit_code != 0
|
|
assert "Template VMX not found" in result.output
|
|
|
|
|
|
def test_vm_new_clone_failure_cleans_up_partial_dir(
|
|
monkeypatch: pytest.MonkeyPatch, fake_template: Path, tmp_path: Path
|
|
) -> None:
|
|
"""Pester migration: vmrun failure removes the partially-created clone dir."""
|
|
base = tmp_path / "build-vms"
|
|
backend = _FakeBackend(fail=True)
|
|
monkeypatch.setattr(vm_module, "_make_backend", lambda _v: backend)
|
|
result = CliRunner().invoke(
|
|
cli,
|
|
[
|
|
"vm",
|
|
"new",
|
|
"--template",
|
|
str(fake_template),
|
|
"--clone-base-dir",
|
|
str(base),
|
|
"--job-id",
|
|
"test-fail",
|
|
],
|
|
)
|
|
assert result.exit_code != 0
|
|
assert "clone failed" in result.output
|
|
leftovers = [p for p in base.iterdir() if p.is_dir() and "test-fail" in p.name]
|
|
assert leftovers == []
|
|
|
|
|
|
def test_vm_new_clone_name_format_and_stdout(
|
|
monkeypatch: pytest.MonkeyPatch, fake_template: Path, tmp_path: Path
|
|
) -> None:
|
|
"""Pester migration: clone folder is ``Clone_{JobId}_{yyyyMMdd_HHmmss}``
|
|
and the final stdout line is the clone VMX path."""
|
|
base = tmp_path / "build-vms"
|
|
backend = _FakeBackend()
|
|
monkeypatch.setattr(vm_module, "_make_backend", lambda _v: backend)
|
|
result = CliRunner().invoke(
|
|
cli,
|
|
[
|
|
"vm",
|
|
"new",
|
|
"--template",
|
|
str(fake_template),
|
|
"--clone-base-dir",
|
|
str(base),
|
|
"--job-id",
|
|
"run-42",
|
|
],
|
|
)
|
|
assert result.exit_code == 0, result.output
|
|
final = result.output.strip().splitlines()[-1]
|
|
assert final.endswith(".vmx")
|
|
assert "Clone_run-42_" in final
|
|
# Format check: timestamp is 8 digits + underscore + 6 digits.
|
|
import re
|
|
|
|
assert re.search(r"Clone_run-42_\d{8}_\d{6}\.vmx$", final)
|
|
|
|
|
|
def test_vm_new_passes_snapshot_default(
|
|
monkeypatch: pytest.MonkeyPatch, fake_template: Path, tmp_path: Path
|
|
) -> None:
|
|
base = tmp_path / "build-vms"
|
|
backend = _FakeBackend()
|
|
monkeypatch.setattr(vm_module, "_make_backend", lambda _v: backend)
|
|
result = CliRunner().invoke(
|
|
cli,
|
|
[
|
|
"vm",
|
|
"new",
|
|
"--template",
|
|
str(fake_template),
|
|
"--clone-base-dir",
|
|
str(base),
|
|
"--job-id",
|
|
"run-99",
|
|
],
|
|
)
|
|
assert result.exit_code == 0, result.output
|
|
assert backend.calls[0]["snapshot"] == "BaseClean"
|
|
|
|
|
|
def test_vm_new_pascal_case_aliases(
|
|
monkeypatch: pytest.MonkeyPatch, fake_template: Path, tmp_path: Path
|
|
) -> None:
|
|
"""The PS-shim translation produces ``--template-path`` / ``--snapshot-name``."""
|
|
base = tmp_path / "build-vms"
|
|
backend = _FakeBackend()
|
|
monkeypatch.setattr(vm_module, "_make_backend", lambda _v: backend)
|
|
result = CliRunner().invoke(
|
|
cli,
|
|
[
|
|
"vm",
|
|
"new",
|
|
"--template-path",
|
|
str(fake_template),
|
|
"--snapshot-name",
|
|
"Custom",
|
|
"--clone-base-dir",
|
|
str(base),
|
|
"--job-id",
|
|
"alias",
|
|
],
|
|
)
|
|
assert result.exit_code == 0, result.output
|
|
assert backend.calls[0]["snapshot"] == "Custom"
|
|
|
|
|
|
def test_vm_new_backend_unavailable(
|
|
monkeypatch: pytest.MonkeyPatch, fake_template: Path, tmp_path: Path
|
|
) -> None:
|
|
from ci_orchestrator.backends.errors import BackendNotAvailable
|
|
|
|
base = tmp_path / "build-vms"
|
|
|
|
def _raise(_v: Any) -> Any:
|
|
raise BackendNotAvailable("no vmrun")
|
|
|
|
monkeypatch.setattr(vm_module, "_make_backend", _raise)
|
|
result = CliRunner().invoke(
|
|
cli,
|
|
[
|
|
"vm", "new",
|
|
"--template", str(fake_template),
|
|
"--clone-base-dir", str(base),
|
|
"--job-id", "novm",
|
|
],
|
|
)
|
|
assert result.exit_code != 0
|
|
assert "vmrun unavailable" in result.output
|
|
|
|
|
|
def test_vm_new_partial_clone_dir_cleaned(
|
|
monkeypatch: pytest.MonkeyPatch, fake_template: Path, tmp_path: Path
|
|
) -> None:
|
|
"""Backend creates the partial dir then fails -> dir is removed."""
|
|
base = tmp_path / "build-vms"
|
|
|
|
class _B:
|
|
def clone_linked(
|
|
self, template: str, snapshot: str, name: str,
|
|
destination: str | None = None,
|
|
) -> Any:
|
|
assert destination is not None
|
|
Path(destination).parent.mkdir(parents=True, exist_ok=True)
|
|
(Path(destination).parent / "stale.txt").write_text("x")
|
|
raise BackendOperationFailed("clone", 1, "boom")
|
|
|
|
monkeypatch.setattr(vm_module, "_make_backend", lambda _v: _B())
|
|
result = CliRunner().invoke(
|
|
cli,
|
|
[
|
|
"vm", "new",
|
|
"--template", str(fake_template),
|
|
"--clone-base-dir", str(base),
|
|
"--job-id", "partial",
|
|
],
|
|
)
|
|
assert result.exit_code != 0
|
|
assert "clone failed" in result.output
|
|
assert not any(p.is_dir() and "partial" in p.name for p in base.iterdir())
|
|
|
|
|
|
def test_vm_new_backend_success_but_missing_vmx(
|
|
monkeypatch: pytest.MonkeyPatch, fake_template: Path, tmp_path: Path
|
|
) -> None:
|
|
"""Backend reports success but VMX file isn't there -> ClickException."""
|
|
base = tmp_path / "build-vms"
|
|
|
|
class _B:
|
|
def clone_linked(
|
|
self, template: str, snapshot: str, name: str,
|
|
destination: str | None = None,
|
|
) -> VmHandle:
|
|
return VmHandle(identifier=str(destination), name=name)
|
|
|
|
monkeypatch.setattr(vm_module, "_make_backend", lambda _v: _B())
|
|
result = CliRunner().invoke(
|
|
cli,
|
|
[
|
|
"vm", "new",
|
|
"--template", str(fake_template),
|
|
"--clone-base-dir", str(base),
|
|
"--job-id", "ghost",
|
|
],
|
|
)
|
|
assert result.exit_code != 0
|
|
assert "clone VMX is missing" in result.output
|
|
|
|
|
|
def test_vm_new_creates_base_dir(
|
|
monkeypatch: pytest.MonkeyPatch, fake_template: Path, tmp_path: Path
|
|
) -> None:
|
|
base = tmp_path / "missing-base"
|
|
backend = _FakeBackend()
|
|
monkeypatch.setattr(vm_module, "_make_backend", lambda _v: backend)
|
|
result = CliRunner().invoke(
|
|
cli,
|
|
[
|
|
"vm",
|
|
"new",
|
|
"--template",
|
|
str(fake_template),
|
|
"--clone-base-dir",
|
|
str(base),
|
|
"--job-id",
|
|
"bd",
|
|
],
|
|
)
|
|
assert result.exit_code == 0, result.output
|
|
assert base.is_dir()
|