Phase A1: bootstrap Python ci_orchestrator package
- pyproject.toml (hatchling) with ruff/mypy/pytest dev deps - src/ci_orchestrator/: config, credentials, backends (Protocol + WorkstationVmrunBackend), transport (WinRM via pypsrp, SSH via paramiko) - CLI entry point with PoC 'wait-ready' subcommand (Windows + Linux guests) - tests/python/: 35 unit tests, 83% coverage, all backends/transport mocked - gitea/workflows/lint.yml: new 'python' job (ruff + mypy --strict + pytest --cov-fail-under=70) - config.example.toml + README setup section + .gitignore Python entries Neutral VmBackend Protocol prepared for Phase C ESXi extension. See plans/implementation-plan-A-B.md step A1.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""Tests for WinRmTransport (pypsrp client mocked)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from ci_orchestrator.transport.errors import TransportCommandError, TransportConnectError
|
||||
from ci_orchestrator.transport.winrm import WinRmTransport
|
||||
|
||||
|
||||
class _FakeClient:
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.copies: list[tuple[str, str]] = []
|
||||
self.fetches: list[tuple[str, str]] = []
|
||||
self.scripts: list[str] = []
|
||||
self.next_result: tuple[str, str, int] = ("ok", "", 0)
|
||||
self.wsman = types.SimpleNamespace(close=lambda: None)
|
||||
|
||||
def execute_ps(self, script: str) -> tuple[str, str, int]:
|
||||
self.scripts.append(script)
|
||||
return self.next_result
|
||||
|
||||
def copy(self, local: str, remote: str) -> None:
|
||||
self.copies.append((local, remote))
|
||||
|
||||
def fetch(self, remote: str, local: str) -> None:
|
||||
self.fetches.append((remote, local))
|
||||
|
||||
|
||||
def _install_fake_pypsrp(monkeypatch: pytest.MonkeyPatch, client: _FakeClient) -> None:
|
||||
module = types.ModuleType("pypsrp")
|
||||
client_module = types.ModuleType("pypsrp.client")
|
||||
client_module.Client = lambda *a, **kw: client # type: ignore[attr-defined]
|
||||
monkeypatch.setitem(sys.modules, "pypsrp", module)
|
||||
monkeypatch.setitem(sys.modules, "pypsrp.client", client_module)
|
||||
|
||||
|
||||
def test_run_returns_result(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
fake = _FakeClient()
|
||||
fake.next_result = ("hello\n", "", 0)
|
||||
_install_fake_pypsrp(monkeypatch, fake)
|
||||
t = WinRmTransport("10.0.0.1", "user", "pwd")
|
||||
result = t.run("Write-Output 'hello'")
|
||||
assert result.ok
|
||||
assert result.stdout == "hello\n"
|
||||
assert fake.scripts == ["Write-Output 'hello'"]
|
||||
|
||||
|
||||
def test_run_raises_on_failure(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
fake = _FakeClient()
|
||||
fake.next_result = ("", "boom", 7)
|
||||
_install_fake_pypsrp(monkeypatch, fake)
|
||||
t = WinRmTransport("10.0.0.1", "user", "pwd")
|
||||
with pytest.raises(TransportCommandError) as exc:
|
||||
t.run("bad")
|
||||
assert exc.value.returncode == 7
|
||||
|
||||
|
||||
def test_run_check_false_does_not_raise(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
fake = _FakeClient()
|
||||
fake.next_result = ("", "boom", 7)
|
||||
_install_fake_pypsrp(monkeypatch, fake)
|
||||
t = WinRmTransport("10.0.0.1", "user", "pwd")
|
||||
result = t.run("bad", check=False)
|
||||
assert not result.ok
|
||||
|
||||
|
||||
def test_copy_and_fetch(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
fake = _FakeClient()
|
||||
_install_fake_pypsrp(monkeypatch, fake)
|
||||
t = WinRmTransport("10.0.0.1", "user", "pwd")
|
||||
t.copy("local.txt", "C:/remote.txt")
|
||||
t.fetch("C:/remote.txt", "downloaded.txt")
|
||||
assert fake.copies == [("local.txt", "C:/remote.txt")]
|
||||
assert fake.fetches == [("C:/remote.txt", "downloaded.txt")]
|
||||
|
||||
|
||||
def test_is_ready_handles_connect_failure(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Make the local import inside _connect succeed but Client raise.
|
||||
module = types.ModuleType("pypsrp")
|
||||
client_module = types.ModuleType("pypsrp.client")
|
||||
|
||||
def _boom(*_a: object, **_kw: object) -> None:
|
||||
raise OSError("connection refused")
|
||||
|
||||
client_module.Client = _boom # type: ignore[attr-defined]
|
||||
monkeypatch.setitem(sys.modules, "pypsrp", module)
|
||||
monkeypatch.setitem(sys.modules, "pypsrp.client", client_module)
|
||||
|
||||
t = WinRmTransport("10.0.0.1", "user", "pwd")
|
||||
assert t.is_ready() is False
|
||||
# Direct .run should raise.
|
||||
with pytest.raises(TransportConnectError):
|
||||
t.run("x")
|
||||
Reference in New Issue
Block a user