Files
local-ci-cd-system/tests/python/test_commands_artifacts.py
T
Simone 816a15503e feat(a3): port build pipeline (vm new, build run, artifacts collect)
Implements Phase A3 of plans/implementation-plan-A-B.md:

- commands/vm.py: vm new (replaces New-BuildVM.ps1)

- commands/build.py: build run (replaces Invoke-RemoteBuild.ps1) with WinRM/SSH dispatch and stdout streaming for act_runner

- commands/artifacts.py: artifacts collect (replaces Get-BuildArtifacts.ps1) using transport.fetch()

- 3 PS scripts reduced to shims preserving \0

- Pester tests/{New-BuildVM,Wait-VMReady,Remove-BuildVM}.Tests.ps1 removed; equivalent cases covered in pytest

- Phase C hook: build/artifacts accept opaque VmHandle/vmx; no Windows path assumptions

Tests: 91 pytest, ruff clean, mypy --strict clean, coverage 78.27% (>=70 gate).

Master checklist + step A3 attivita + 'definizione di fatto' updated; A3-closeout.md added.
2026-05-14 18:34:03 +02:00

290 lines
9.2 KiB
Python

"""Tests for ``ci_orchestrator artifacts collect``."""
from __future__ import annotations
import json
import tarfile
from pathlib import Path
from typing import Any, ClassVar
import pytest
from click.testing import CliRunner
import ci_orchestrator.commands.artifacts as artifacts_module
from ci_orchestrator.__main__ import cli
from ci_orchestrator.credentials import Credential
from ci_orchestrator.transport.errors import TransportCommandError
class _FakeResult:
def __init__(self, stdout: str = "OK", returncode: int = 0) -> None:
self.stdout = stdout
self.stderr = ""
self.returncode = returncode
@property
def ok(self) -> bool:
return self.returncode == 0
class _FakeTransport:
instances: ClassVar[list[_FakeTransport]] = []
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.runs: list[str] = []
self.fetched: list[tuple[str, str]] = []
self.fetch_payloads: dict[str, bytes] = {}
self.run_overrides: dict[str, _FakeResult] = {}
type(self).instances.append(self)
def __enter__(self) -> _FakeTransport:
return self
def __exit__(self, *_e: object) -> None:
return None
def run(self, script: str, *, check: bool = True) -> _FakeResult:
self.runs.append(script)
for key, override in self.run_overrides.items():
if key in script:
if check and not override.ok:
raise TransportCommandError(
override.returncode, override.stdout, override.stderr
)
return override
return _FakeResult()
def fetch(self, remote_path: str, local_path: str) -> None:
self.fetched.append((remote_path, str(local_path)))
payload = self.fetch_payloads.get(remote_path, b"data")
Path(local_path).write_bytes(payload)
@pytest.fixture(autouse=True)
def _reset_instances() -> None:
_FakeTransport.instances = []
def _patch(monkeypatch: pytest.MonkeyPatch) -> None:
class _Store:
def get(self, _t: str) -> Credential:
return Credential("ci", "pwd")
monkeypatch.setattr(artifacts_module, "WinRmTransport", _FakeTransport)
monkeypatch.setattr(artifacts_module, "SshTransport", _FakeTransport)
monkeypatch.setattr(artifacts_module, "KeyringCredentialStore", lambda: _Store())
# ── Windows happy path ────────────────────────────────────────────────────
def test_collect_windows_happy_path(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
_patch(monkeypatch)
out_dir = tmp_path / "artifacts"
result = CliRunner().invoke(
cli,
[
"artifacts",
"collect",
"--ip-address",
"10.0.0.7",
"--guest-artifact-path",
"C:\\CI\\output\\artifacts.zip",
"--host-artifact-dir",
str(out_dir),
],
)
assert result.exit_code == 0, result.output
assert (out_dir / "artifacts.zip").is_file()
instance = _FakeTransport.instances[0]
assert any("Test-Path" in c for c in instance.runs)
assert instance.fetched == [
("C:\\CI\\output\\artifacts.zip", str(out_dir / "artifacts.zip"))
]
def test_collect_windows_artifact_missing(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
_patch(monkeypatch)
class _Missing(_FakeTransport):
def __init__(self, *a: Any, **kw: Any) -> None:
super().__init__(*a, **kw)
self.run_overrides["Test-Path"] = _FakeResult(stdout="MISSING\r\n")
monkeypatch.setattr(artifacts_module, "WinRmTransport", _Missing)
result = CliRunner().invoke(
cli,
[
"artifacts",
"collect",
"--ip-address",
"10.0.0.7",
"--guest-artifact-path",
"C:\\CI\\output\\artifacts.zip",
"--host-artifact-dir",
str(tmp_path / "out"),
],
)
assert result.exit_code != 0
assert "artifact not found" in result.output
def test_collect_windows_include_logs(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
_patch(monkeypatch)
class _WithLogs(_FakeTransport):
def __init__(self, *a: Any, **kw: Any) -> None:
super().__init__(*a, **kw)
self.run_overrides["Get-ChildItem"] = _FakeResult(
stdout="C:\\CI\\build\\one.log\r\nC:\\CI\\build\\sub\\two.log\r\n"
)
monkeypatch.setattr(artifacts_module, "WinRmTransport", _WithLogs)
result = CliRunner().invoke(
cli,
[
"artifacts",
"collect",
"--ip-address",
"10.0.0.7",
"--guest-artifact-path",
"C:\\CI\\output\\artifacts.zip",
"--host-artifact-dir",
str(tmp_path / "out"),
"--include-logs",
],
)
assert result.exit_code == 0, result.output
fetched_remotes = [r for r, _ in _FakeTransport.instances[0].fetched]
assert "C:\\CI\\build\\one.log" in fetched_remotes
assert "C:\\CI\\build\\sub\\two.log" in fetched_remotes
def test_collect_writes_manifest_when_job_id_set(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
_patch(monkeypatch)
out = tmp_path / "out"
result = CliRunner().invoke(
cli,
[
"artifacts",
"collect",
"--ip-address",
"10.0.0.7",
"--guest-artifact-path",
"C:\\CI\\output\\artifacts.zip",
"--host-artifact-dir",
str(out),
"--job-id",
"run-42",
"--commit",
"abc1234",
],
)
assert result.exit_code == 0, result.output
manifest_path = out / "manifest.json"
assert manifest_path.is_file()
data = json.loads(manifest_path.read_text(encoding="utf-8"))
assert data["jobId"] == "run-42"
assert data["commit"] == "abc1234"
names = [f["name"] for f in data["files"]]
assert "artifacts.zip" in names
# ── Linux happy path ──────────────────────────────────────────────────────
def test_collect_linux_extracts_tarball(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
_patch(monkeypatch)
# Build a real tarball that the fake transport will "fetch".
payload_dir = tmp_path / "payload"
payload_dir.mkdir()
(payload_dir / "binary.bin").write_bytes(b"\x01\x02\x03")
(payload_dir / "log.txt").write_text("hello", encoding="utf-8")
tar_local = tmp_path / "src.tar.gz"
with tarfile.open(tar_local, "w:gz") as tf:
for f in payload_dir.iterdir():
tf.add(f, arcname=f.name)
payload_bytes = tar_local.read_bytes()
class _LinuxTransport(_FakeTransport):
def __init__(self, *a: Any, **kw: Any) -> None:
super().__init__(*a, **kw)
# Any fetched path returns the same tarball payload.
self.fetch_payloads["__default__"] = payload_bytes
def fetch(self, remote_path: str, local_path: str) -> None:
self.fetched.append((remote_path, str(local_path)))
Path(local_path).write_bytes(payload_bytes)
monkeypatch.setattr(artifacts_module, "SshTransport", _LinuxTransport)
out = tmp_path / "out"
result = CliRunner().invoke(
cli,
[
"artifacts",
"collect",
"--ip-address",
"10.0.0.5",
"--guest-os",
"linux",
"--guest-artifact-path",
"/opt/ci/output",
"--host-artifact-dir",
str(out),
"--ssh-key-path",
str(tmp_path / "key"),
],
)
assert result.exit_code == 0, result.output
assert (out / "binary.bin").read_bytes() == b"\x01\x02\x03"
assert (out / "log.txt").read_text(encoding="utf-8") == "hello"
cmds = _FakeTransport.instances[0].runs
assert any("tar -czf" in c for c in cmds)
# Cleanup of the temp tarball on the guest was attempted.
assert any("rm -f" in c for c in cmds)
def test_collect_linux_no_files_raises(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
_patch(monkeypatch)
# Empty tarball => extraction yields nothing.
empty_tar = tmp_path / "empty.tar.gz"
with tarfile.open(empty_tar, "w:gz"):
pass
payload = empty_tar.read_bytes()
class _Empty(_FakeTransport):
def fetch(self, remote_path: str, local_path: str) -> None:
self.fetched.append((remote_path, str(local_path)))
Path(local_path).write_bytes(payload)
monkeypatch.setattr(artifacts_module, "SshTransport", _Empty)
result = CliRunner().invoke(
cli,
[
"artifacts",
"collect",
"--ip-address",
"10.0.0.5",
"--guest-os",
"linux",
"--guest-artifact-path",
"/opt/ci/output",
"--host-artifact-dir",
str(tmp_path / "out"),
"--ssh-key-path",
str(tmp_path / "key"),
],
)
assert result.exit_code != 0
assert "no files were transferred" in result.output