feat(cli): port burn-in/smoke/validate/creds to Python (Phase C1-C6)
Add four native command groups so the Linux host no longer needs pwsh for
manual ops. __main__ registers all four at once, so they ship together.
- bench run (C2): N concurrent jobs in-process via concurrent.futures,
per-round orphan-clone + stale-lock assertions. Replaces
Test-CapacityBurnIn.ps1 + Start-BurnInTest*.ps1. No
Start-Job/pwsh/$IsWindows.
- bench measure (C3): clone/start/IP/transport/destroy timings appended to
benchmark.jsonl (legacy field names kept). Replaces
Measure-CIBenchmark.ps1.
- smoke run (C4): one E2E job; asserts exit 0 + artifact dir + job/success
event. ns7zip/nsinnounp presets. Replaces Test-Smoke.ps1
+ Test-*Build-Linux.ps1.
- validate host (C5): vmrun/snapshot/keyring/permissions/systemd checks.
- validate guest(C6): transport probe with explicit failure cause.
- creds set (C6): keyring writer matching credentials.py read scheme;
password via stdin/prompt only. Replaces
Set-CIGuestCredential.ps1.
All groups import backends.load_backend only (Phase D ESXi path stays open).
Suite green, coverage 95.10% (gate 90%); new modules at 100%.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,542 @@
|
||||
"""Tests for ``validate host`` (C5) and ``validate guest`` (C6).
|
||||
|
||||
CLI registration smoke tests (``--help``) are kept; behavioural tests mock
|
||||
the backend, keyring, systemctl, and transports.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
import ci_orchestrator.commands.validate as validate_module
|
||||
from ci_orchestrator.__main__ import cli
|
||||
from ci_orchestrator.backends.errors import (
|
||||
BackendNotAvailable,
|
||||
BackendOperationFailed,
|
||||
)
|
||||
from ci_orchestrator.config import BackendConfig, Config, Paths
|
||||
from ci_orchestrator.credentials import Credential
|
||||
from ci_orchestrator.transport.errors import (
|
||||
TransportCommandError,
|
||||
TransportConnectError,
|
||||
)
|
||||
|
||||
# ── --help smoke tests (kept) ───────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_validate_help_lists_subcommands() -> None:
|
||||
result = CliRunner().invoke(cli, ["validate", "--help"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "host" in result.output
|
||||
assert "guest" in result.output
|
||||
|
||||
|
||||
def test_validate_guest_help() -> None:
|
||||
result = CliRunner().invoke(cli, ["validate", "guest", "--help"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "--host" in result.output
|
||||
|
||||
|
||||
# ── helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _make_config(tmp_path: Path) -> Config:
|
||||
root = tmp_path / "ci"
|
||||
templates = root / "templates"
|
||||
build_vms = root / "build-vms"
|
||||
artifacts = root / "artifacts"
|
||||
keys = root / "keys"
|
||||
for p in (root, templates, build_vms, artifacts, keys):
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
return Config(
|
||||
paths=Paths(
|
||||
root=root,
|
||||
templates=templates,
|
||||
build_vms=build_vms,
|
||||
artifacts=artifacts,
|
||||
keys=keys,
|
||||
),
|
||||
backend=BackendConfig(type="workstation"),
|
||||
guest_cred_target="BuildVMGuest",
|
||||
)
|
||||
|
||||
|
||||
class _FakeBackend:
|
||||
def __init__(self, snapshots: dict[str, list[str]] | None = None) -> None:
|
||||
self.vmrun_path = "/usr/bin/vmrun"
|
||||
self._snapshots = snapshots or {}
|
||||
|
||||
def list_snapshots(self, handle: Any) -> list[str]:
|
||||
return self._snapshots.get(handle.identifier, [])
|
||||
|
||||
|
||||
def _make_template_vmx(config: Config, name: str) -> str:
|
||||
vmx_dir = config.paths.templates / name
|
||||
vmx_dir.mkdir(parents=True, exist_ok=True)
|
||||
vmx = vmx_dir / f"{name}.vmx"
|
||||
vmx.write_text('config.version = "8"', encoding="utf-8")
|
||||
return str(vmx)
|
||||
|
||||
|
||||
# ── validate host: individual checks ────────────────────────────────────────
|
||||
|
||||
|
||||
def test_check_vmrun_ok(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
monkeypatch.setattr(validate_module, "load_backend", lambda _c: _FakeBackend())
|
||||
result = validate_module._check_vmrun(config)
|
||||
assert result.ok
|
||||
assert "vmrun" in result.detail
|
||||
|
||||
|
||||
def test_check_vmrun_no_path_attr(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
|
||||
class _NoPath:
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(validate_module, "load_backend", lambda _c: _NoPath())
|
||||
result = validate_module._check_vmrun(config)
|
||||
assert result.ok
|
||||
assert "backend loaded" in result.detail
|
||||
|
||||
|
||||
def test_check_vmrun_unavailable(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
|
||||
def _raise(_c: Any) -> Any:
|
||||
raise BackendNotAvailable("vmrun missing")
|
||||
|
||||
monkeypatch.setattr(validate_module, "load_backend", _raise)
|
||||
result = validate_module._check_vmrun(config)
|
||||
assert not result.ok
|
||||
assert "missing" in result.detail
|
||||
|
||||
|
||||
def test_check_snapshots_backend_unavailable(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
|
||||
def _raise(_c: Any) -> Any:
|
||||
raise BackendNotAvailable("no vmrun")
|
||||
|
||||
monkeypatch.setattr(validate_module, "load_backend", _raise)
|
||||
result = validate_module._check_snapshots(config)
|
||||
assert not result.ok
|
||||
assert "backend unavailable" in result.detail
|
||||
|
||||
|
||||
def test_check_snapshots_list_raises(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
_make_template_vmx(config, "LinuxBuild2404")
|
||||
|
||||
class _Backend:
|
||||
vmrun_path = "/usr/bin/vmrun"
|
||||
|
||||
def list_snapshots(self, _handle: Any) -> list[str]:
|
||||
raise BackendOperationFailed("listSnapshots", 1, "boom")
|
||||
|
||||
monkeypatch.setattr(validate_module, "load_backend", lambda _c: _Backend())
|
||||
result = validate_module._check_snapshots(config)
|
||||
assert not result.ok
|
||||
assert "listSnapshots failed" in result.detail
|
||||
|
||||
|
||||
def test_check_keyring_runtime_error(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
|
||||
class _Store:
|
||||
def get(self, target: str) -> Credential:
|
||||
raise RuntimeError("keyring is not installed")
|
||||
|
||||
monkeypatch.setattr(validate_module, "KeyringCredentialStore", _Store)
|
||||
result = validate_module._check_keyring(config)
|
||||
assert not result.ok
|
||||
assert "keyring is not installed" in result.detail
|
||||
|
||||
|
||||
def test_check_permissions_not_writable(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
|
||||
def _no_access(_path: Any, _mode: int) -> bool:
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(validate_module.os, "access", _no_access)
|
||||
result = validate_module._check_permissions(config)
|
||||
assert not result.ok
|
||||
assert "not read/write" in result.detail
|
||||
|
||||
|
||||
def test_check_snapshots_ok(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
win_vmx = _make_template_vmx(config, "WinBuild2025")
|
||||
backend = _FakeBackend({win_vmx: ["BaseClean"]})
|
||||
monkeypatch.setattr(validate_module, "load_backend", lambda _c: backend)
|
||||
result = validate_module._check_snapshots(config)
|
||||
assert result.ok, result.detail
|
||||
assert "WinBuild2025->BaseClean" in result.detail
|
||||
|
||||
|
||||
def test_check_snapshots_missing_snapshot(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
lin_vmx = _make_template_vmx(config, "LinuxBuild2404")
|
||||
backend = _FakeBackend({lin_vmx: ["SomethingElse"]})
|
||||
monkeypatch.setattr(validate_module, "load_backend", lambda _c: backend)
|
||||
result = validate_module._check_snapshots(config)
|
||||
assert not result.ok
|
||||
assert "BaseClean-Linux" in result.detail
|
||||
|
||||
|
||||
def test_check_snapshots_no_templates(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
backend = _FakeBackend({})
|
||||
monkeypatch.setattr(validate_module, "load_backend", lambda _c: backend)
|
||||
result = validate_module._check_snapshots(config)
|
||||
assert not result.ok
|
||||
assert "no known template VMX found" in result.detail
|
||||
|
||||
|
||||
def test_check_keyring_ok(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
|
||||
class _Store:
|
||||
def get(self, target: str) -> Credential:
|
||||
return Credential(username="ci_build", password="x")
|
||||
|
||||
monkeypatch.setattr(validate_module, "KeyringCredentialStore", _Store)
|
||||
result = validate_module._check_keyring(config)
|
||||
assert result.ok
|
||||
assert "ci_build" in result.detail
|
||||
|
||||
|
||||
def test_check_keyring_missing(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
|
||||
class _Store:
|
||||
def get(self, target: str) -> Credential:
|
||||
raise KeyError(target)
|
||||
|
||||
monkeypatch.setattr(validate_module, "KeyringCredentialStore", _Store)
|
||||
result = validate_module._check_keyring(config)
|
||||
assert not result.ok
|
||||
|
||||
|
||||
def test_check_permissions_ok(tmp_path: Path) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
result = validate_module._check_permissions(config)
|
||||
assert result.ok, result.detail
|
||||
|
||||
|
||||
def test_check_permissions_missing_dir(tmp_path: Path) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
# Remove a required dir.
|
||||
config.paths.build_vms.rmdir()
|
||||
result = validate_module._check_permissions(config)
|
||||
assert not result.ok
|
||||
assert "missing" in result.detail
|
||||
|
||||
|
||||
def test_check_units_no_systemctl(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(validate_module.shutil, "which", lambda _x: None)
|
||||
result = validate_module._check_units()
|
||||
assert not result.ok
|
||||
assert "systemctl" in result.detail
|
||||
|
||||
|
||||
def test_check_units_active(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(validate_module.shutil, "which", lambda _x: "/bin/systemctl")
|
||||
monkeypatch.setattr(validate_module, "_systemctl_is_active", lambda _u: "active")
|
||||
result = validate_module._check_units()
|
||||
assert result.ok
|
||||
|
||||
|
||||
def test_check_units_inactive(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(validate_module.shutil, "which", lambda _x: "/bin/systemctl")
|
||||
monkeypatch.setattr(validate_module, "_systemctl_is_active", lambda _u: "inactive")
|
||||
result = validate_module._check_units()
|
||||
assert not result.ok
|
||||
assert "inactive" in result.detail
|
||||
|
||||
|
||||
def test_systemctl_is_active_invokes_subprocess(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(validate_module.shutil, "which", lambda _x: "/bin/systemctl")
|
||||
|
||||
class _CP:
|
||||
stdout = "active\n"
|
||||
|
||||
monkeypatch.setattr(
|
||||
validate_module.subprocess, "run", lambda *a, **k: _CP()
|
||||
)
|
||||
assert validate_module._systemctl_is_active("act-runner.service") == "active"
|
||||
|
||||
|
||||
def test_systemctl_is_active_no_binary(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(validate_module.shutil, "which", lambda _x: None)
|
||||
assert validate_module._systemctl_is_active("x") is None
|
||||
|
||||
|
||||
def test_systemctl_is_active_timeout(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
import subprocess as _sp
|
||||
|
||||
monkeypatch.setattr(validate_module.shutil, "which", lambda _x: "/bin/systemctl")
|
||||
|
||||
def _boom(*_a: Any, **_k: Any) -> Any:
|
||||
raise _sp.TimeoutExpired(cmd="systemctl", timeout=10.0)
|
||||
|
||||
monkeypatch.setattr(validate_module.subprocess, "run", _boom)
|
||||
assert validate_module._systemctl_is_active("x") is None
|
||||
|
||||
|
||||
# ── validate host: end-to-end command ───────────────────────────────────────
|
||||
|
||||
|
||||
def test_validate_host_all_pass(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
win_vmx = _make_template_vmx(config, "WinBuild2025")
|
||||
backend = _FakeBackend({win_vmx: ["BaseClean"]})
|
||||
|
||||
class _Store:
|
||||
def get(self, target: str) -> Credential:
|
||||
return Credential("ci_build", "x")
|
||||
|
||||
monkeypatch.setattr(validate_module, "load_config", lambda: config)
|
||||
monkeypatch.setattr(validate_module, "load_backend", lambda _c: backend)
|
||||
monkeypatch.setattr(validate_module, "KeyringCredentialStore", _Store)
|
||||
monkeypatch.setattr(validate_module.shutil, "which", lambda _x: "/bin/systemctl")
|
||||
monkeypatch.setattr(validate_module, "_systemctl_is_active", lambda _u: "active")
|
||||
|
||||
result = CliRunner().invoke(cli, ["validate", "host"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "All host checks passed" in result.output
|
||||
|
||||
|
||||
def test_validate_host_reports_failures_and_exits_nonzero(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
|
||||
def _raise(_c: Any) -> Any:
|
||||
raise BackendNotAvailable("vmrun missing")
|
||||
|
||||
class _Store:
|
||||
def get(self, target: str) -> Credential:
|
||||
raise KeyError(target)
|
||||
|
||||
monkeypatch.setattr(validate_module, "load_config", lambda: config)
|
||||
monkeypatch.setattr(validate_module, "load_backend", _raise)
|
||||
monkeypatch.setattr(validate_module, "KeyringCredentialStore", _Store)
|
||||
monkeypatch.setattr(validate_module.shutil, "which", lambda _x: None)
|
||||
|
||||
result = CliRunner().invoke(cli, ["validate", "host"])
|
||||
assert result.exit_code == 1, result.output
|
||||
assert "FAIL" in result.output
|
||||
assert "check(s) failed" in result.output
|
||||
|
||||
|
||||
# ── validate guest ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_resolve_use_winrm_explicit() -> None:
|
||||
assert validate_module._resolve_use_winrm(True, None) is True
|
||||
assert validate_module._resolve_use_winrm(False, "windows") is False
|
||||
|
||||
|
||||
def test_resolve_use_winrm_inferred() -> None:
|
||||
assert validate_module._resolve_use_winrm(None, "linux") is False
|
||||
assert validate_module._resolve_use_winrm(None, "windows") is True
|
||||
|
||||
|
||||
def test_resolve_use_winrm_requires_hint() -> None:
|
||||
import click as _click
|
||||
|
||||
with pytest.raises(_click.UsageError):
|
||||
validate_module._resolve_use_winrm(None, None)
|
||||
|
||||
|
||||
class _FakeTransport:
|
||||
"""Records connect/run/close and raises a configured error on run."""
|
||||
|
||||
def __init__(self, *args: Any, raise_on_run: Exception | None = None, **kw: Any) -> None:
|
||||
self._raise = raise_on_run
|
||||
self.closed = False
|
||||
|
||||
def run(self, *_a: Any, **_k: Any) -> Any:
|
||||
if self._raise is not None:
|
||||
raise self._raise
|
||||
|
||||
class _R:
|
||||
returncode = 0
|
||||
|
||||
return _R()
|
||||
|
||||
def close(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
|
||||
def _install_keyring_store(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
class _Store:
|
||||
def get(self, target: str) -> Credential:
|
||||
return Credential("Administrator", "pw")
|
||||
|
||||
monkeypatch.setattr(validate_module, "KeyringCredentialStore", _Store)
|
||||
|
||||
|
||||
def test_probe_winrm_ok(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
_install_keyring_store(monkeypatch)
|
||||
transport = _FakeTransport()
|
||||
monkeypatch.setattr(validate_module, "WinRmTransport", lambda *a, **k: transport)
|
||||
result = validate_module._probe_winrm("10.0.0.5", config)
|
||||
assert result.ok
|
||||
assert transport.closed
|
||||
|
||||
|
||||
def test_probe_winrm_auth_fail(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
_install_keyring_store(monkeypatch)
|
||||
transport = _FakeTransport(raise_on_run=TransportConnectError("auth denied"))
|
||||
monkeypatch.setattr(validate_module, "WinRmTransport", lambda *a, **k: transport)
|
||||
result = validate_module._probe_winrm("10.0.0.5", config)
|
||||
assert not result.ok
|
||||
assert "auth denied" in result.detail
|
||||
|
||||
|
||||
def test_probe_winrm_command_fail(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
_install_keyring_store(monkeypatch)
|
||||
transport = _FakeTransport(raise_on_run=TransportCommandError(1, "", "boom"))
|
||||
monkeypatch.setattr(validate_module, "WinRmTransport", lambda *a, **k: transport)
|
||||
result = validate_module._probe_winrm("10.0.0.5", config)
|
||||
assert not result.ok
|
||||
assert "command failed" in result.detail
|
||||
|
||||
|
||||
def test_probe_winrm_credential_missing(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
|
||||
class _Store:
|
||||
def get(self, target: str) -> Credential:
|
||||
raise KeyError(target)
|
||||
|
||||
monkeypatch.setattr(validate_module, "KeyringCredentialStore", _Store)
|
||||
result = validate_module._probe_winrm("10.0.0.5", config)
|
||||
assert not result.ok
|
||||
assert "credential lookup failed" in result.detail
|
||||
|
||||
|
||||
def test_probe_ssh_ok(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
transport = _FakeTransport()
|
||||
monkeypatch.setattr(validate_module, "SshTransport", lambda *a, **k: transport)
|
||||
result = validate_module._probe_ssh("10.0.0.6", config)
|
||||
assert result.ok
|
||||
assert transport.closed
|
||||
|
||||
|
||||
def test_probe_ssh_connect_fail(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
transport = _FakeTransport(raise_on_run=TransportConnectError("no route"))
|
||||
monkeypatch.setattr(validate_module, "SshTransport", lambda *a, **k: transport)
|
||||
result = validate_module._probe_ssh("10.0.0.6", config)
|
||||
assert not result.ok
|
||||
assert "no route" in result.detail
|
||||
|
||||
|
||||
def test_probe_ssh_command_fail(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
transport = _FakeTransport(raise_on_run=TransportCommandError(2, "", "nope"))
|
||||
monkeypatch.setattr(validate_module, "SshTransport", lambda *a, **k: transport)
|
||||
result = validate_module._probe_ssh("10.0.0.6", config)
|
||||
assert not result.ok
|
||||
assert "command failed" in result.detail
|
||||
|
||||
|
||||
def test_validate_guest_ssh_ok(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
transport = _FakeTransport()
|
||||
monkeypatch.setattr(validate_module, "load_config", lambda: config)
|
||||
monkeypatch.setattr(validate_module, "SshTransport", lambda *a, **k: transport)
|
||||
result = CliRunner().invoke(
|
||||
cli, ["validate", "guest", "--host", "10.0.0.6", "--ssh"]
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "OK" in result.output
|
||||
|
||||
|
||||
def test_validate_guest_winrm_fail_exits_nonzero(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
_install_keyring_store(monkeypatch)
|
||||
transport = _FakeTransport(raise_on_run=TransportConnectError("bad creds"))
|
||||
monkeypatch.setattr(validate_module, "load_config", lambda: config)
|
||||
monkeypatch.setattr(validate_module, "WinRmTransport", lambda *a, **k: transport)
|
||||
result = CliRunner().invoke(
|
||||
cli, ["validate", "guest", "--host", "10.0.0.5", "--winrm"]
|
||||
)
|
||||
assert result.exit_code == 1, result.output
|
||||
assert "FAIL" in result.output
|
||||
|
||||
|
||||
def test_validate_guest_requires_transport_hint(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
monkeypatch.setattr(validate_module, "load_config", lambda: config)
|
||||
result = CliRunner().invoke(cli, ["validate", "guest", "--host", "10.0.0.5"])
|
||||
assert result.exit_code != 0
|
||||
assert "transport" in result.output.lower()
|
||||
|
||||
|
||||
def test_validate_guest_infers_from_guest_os(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
config = _make_config(tmp_path)
|
||||
transport = _FakeTransport()
|
||||
monkeypatch.setattr(validate_module, "load_config", lambda: config)
|
||||
monkeypatch.setattr(validate_module, "SshTransport", lambda *a, **k: transport)
|
||||
result = CliRunner().invoke(
|
||||
cli, ["validate", "guest", "--host", "10.0.0.6", "--guest-os", "linux"]
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
Reference in New Issue
Block a user