Add coverage gap tests for command functionalities
Lint / pssa (push) Successful in 10s
Lint / python (push) Successful in 1m22s

- Implement tests for disk alert JSON output to include a message field when below threshold.
- Add tests for runner status when Gitea is online but no webhook URL is provided.
- Introduce tests for job report details to ensure no error line appears when there are no failure events.
- Add tests to skip empty JSONL files in job report listing.
This commit is contained in:
2026-05-26 21:18:20 +02:00
parent 0a1c455e54
commit 8dc7a4fb99
5 changed files with 1634 additions and 1 deletions
+392 -1
View File
@@ -10,8 +10,9 @@ from click.testing import CliRunner
import ci_orchestrator.commands.build as build_module
from ci_orchestrator.__main__ import cli
from ci_orchestrator.config import BackendConfig, Config, Paths
from ci_orchestrator.credentials import Credential
from ci_orchestrator.transport.errors import TransportCommandError
from ci_orchestrator.transport.errors import TransportCommandError, TransportError
class _FakeResult:
@@ -370,3 +371,393 @@ def test_build_run_windows_host_source_dir(
# Staging copies the raw source into the collect dir (no zip-in-zip).
assert any("Copy-Item" in c and "'dist'" in c for c in cmds)
assert not any("Compress-Archive" in c and "'dist'" in c for c in cmds)
# ── _parse_extra_env unit tests ────────────────────────────────────────────
def test_parse_extra_env_empty_string_skipped() -> None:
"""Line 62: empty strings in the iterable are silently skipped (continue branch)."""
result = build_module._parse_extra_env(["", "FOO=bar", ""])
assert result == {"FOO": "bar"}
def test_parse_extra_env_no_equals_raises() -> None:
"""Line 63-66: value with no '=' must raise BadParameter."""
import click
with pytest.raises(click.BadParameter, match="KEY=VALUE"):
build_module._parse_extra_env(["BADVALUE"])
def test_parse_extra_env_empty_key_raises() -> None:
"""Line 69: '=value' with empty key must raise BadParameter."""
import click
with pytest.raises(click.BadParameter, match="key cannot be empty"):
build_module._parse_extra_env(["=value"])
# ── _zip_dir skips subdirectories ─────────────────────────────────────────
def test_zip_dir_skips_subdirectories(tmp_path: Path) -> None:
"""Line 80->79: _zip_dir iterates rglob('*'); directories are skipped (is_file() == False)."""
import zipfile
src = tmp_path / "src"
src.mkdir()
(src / "file.txt").write_text("hello", encoding="utf-8")
subdir = src / "subdir"
subdir.mkdir()
(subdir / "nested.txt").write_text("world", encoding="utf-8")
archive = tmp_path / "out.zip"
build_module._zip_dir(src, archive)
with zipfile.ZipFile(archive) as zf:
names = zf.namelist()
# Only files should appear; the bare subdirectory entry must not be present.
assert "file.txt" in names
assert str(Path("subdir") / "nested.txt") in names or "subdir/nested.txt" in names
# No entry that ends in '/' (a directory entry).
assert not any(n.endswith("/") for n in names)
# ── _report_build_output: stdout empty, stderr present ────────────────────
def test_report_build_output_stderr_only(capsys: pytest.CaptureFixture[str]) -> None:
"""Lines 108->111, 112-113: when stdout is empty, only stderr branch executes."""
result = build_module._report_build_output("", "some error text")
captured = capsys.readouterr()
assert "some error text" in captured.err
assert captured.out.count("[build run]") == 2 # header + footer lines
# The tail should reflect the stderr content.
assert "some error text" in result
# ── _linux_build: neither host_source_dir nor clone_url ───────────────────
def test_linux_build_no_source_raises(monkeypatch: pytest.MonkeyPatch) -> None:
"""Line 189: _linux_build with no source raises ClickException."""
import click
monkeypatch.setattr(build_module, "SshTransport", _FakeTransport)
with pytest.raises(click.ClickException, match="either --host-source-dir or --clone-url"):
build_module._linux_build(
ip_address="10.0.0.1",
ssh_user="ci_build",
key_path=None,
known_hosts=None,
workdir="/opt/ci/build",
output_dir="/opt/ci/output",
host_source_dir=None,
clone_url=None,
clone_branch="main",
clone_commit="",
clone_submodules=False,
build_command="make",
artifact_source="dist",
extra_env={},
skip_artifact=False,
)
# ── _linux_build: clone_url + clone_commit triggers fetch+checkout ─────────
def test_linux_build_clone_commit_fetch_checkout(monkeypatch: pytest.MonkeyPatch) -> None:
"""Lines 230-233: clone_commit causes 3 subprocess calls (clone, fetch, checkout) via SSH."""
monkeypatch.setattr(build_module, "SshTransport", _FakeTransport)
build_module._linux_build(
ip_address="10.0.0.1",
ssh_user="ci_build",
key_path=None,
known_hosts=None,
workdir="/opt/ci/build",
output_dir="/opt/ci/output",
host_source_dir=None,
clone_url="http://repo/example.git",
clone_branch="main",
clone_commit="abc123",
clone_submodules=False,
build_command="true",
artifact_source="dist",
extra_env={},
skip_artifact=True,
)
instance = _FakeTransport.instances[0]
cmds = [c[0] for c in instance.runs]
assert any("git clone" in c for c in cmds)
assert any("fetch --depth 1 origin" in c and "abc123" in c for c in cmds)
assert any("checkout" in c and "abc123" in c for c in cmds)
# ── _windows_build: clone_url + clone_commit triggers fetch+checkout ───────
def test_windows_build_clone_commit_fetch_checkout(monkeypatch: pytest.MonkeyPatch) -> None:
"""Lines 315-321: clone_commit in windows path causes fetch+checkout run."""
_patch(monkeypatch)
build_module._windows_build(
ip_address="10.0.0.7",
credential_target="BuildVMGuest",
workdir="C:\\CI\\build",
output_dir="C:\\CI\\output",
host_source_dir=None,
clone_url="https://example/repo.git",
clone_branch="main",
clone_commit="deadbeef",
clone_submodules=False,
build_command="echo done",
artifact_source="dist",
configuration="Release",
extra_env={},
skip_artifact=True,
use_shared_cache=False,
)
instance = _FakeTransport.instances[0]
cmds = [c[0] for c in instance.runs]
assert any("git clone" in c for c in cmds)
assert any("fetch --depth 1 origin" in c and "deadbeef" in c for c in cmds)
assert any("checkout" in c and "deadbeef" in c for c in cmds)
# ── _windows_build: no source raises ClickException ────────────────────────
def test_windows_build_no_source_raises(monkeypatch: pytest.MonkeyPatch) -> None:
"""Line 321: _windows_build with no source raises ClickException."""
import click
_patch(monkeypatch)
with pytest.raises(click.ClickException, match="either --host-source-dir or --clone-url"):
build_module._windows_build(
ip_address="10.0.0.7",
credential_target="BuildVMGuest",
workdir="C:\\CI\\build",
output_dir="C:\\CI\\output",
host_source_dir=None,
clone_url=None,
clone_branch="main",
clone_commit="",
clone_submodules=False,
build_command="echo done",
artifact_source="dist",
configuration="Release",
extra_env={},
skip_artifact=False,
use_shared_cache=False,
)
def test_windows_build_extra_env_semicolon_appended(monkeypatch: pytest.MonkeyPatch) -> None:
"""Line 330: when extra_env is non-empty, env_setup gets '; ' appended before build command."""
_patch(monkeypatch)
build_module._windows_build(
ip_address="10.0.0.7",
credential_target="BuildVMGuest",
workdir="C:\\CI\\build",
output_dir="C:\\CI\\output",
host_source_dir=None,
clone_url="https://example/repo.git",
clone_branch="main",
clone_commit="",
clone_submodules=False,
build_command="echo done",
artifact_source="dist",
configuration="Release",
extra_env={"MY_VAR": "hello"},
skip_artifact=True,
use_shared_cache=False,
)
instance = _FakeTransport.instances[0]
cmds = [c[0] for c in instance.runs]
# The build command should contain the injected env var followed by '; '
assert any("$env:MY_VAR = 'hello'" in c for c in cmds)
def test_windows_build_build_failure_raises(monkeypatch: pytest.MonkeyPatch) -> None:
"""Lines 365-367: when the build command returns non-zero, ClickException is raised."""
import click
_patch(monkeypatch)
class _FailsBuild(_FakeTransport):
def run(self, script: str, *, check: bool = True) -> _FakeResult:
if "echo fail" in script:
return _FakeResult(stdout="", stderr="boom", returncode=1)
return super().run(script, check=check)
monkeypatch.setattr(build_module, "WinRmTransport", _FailsBuild)
with pytest.raises(click.ClickException, match="build command failed"):
build_module._windows_build(
ip_address="10.0.0.7",
credential_target="BuildVMGuest",
workdir="C:\\CI\\build",
output_dir="C:\\CI\\output",
host_source_dir=None,
clone_url="https://example/repo.git",
clone_branch="main",
clone_commit="",
clone_submodules=False,
build_command="echo fail",
artifact_source="dist",
configuration="Release",
extra_env={},
skip_artifact=False,
use_shared_cache=False,
)
# ── _linux_build: artifact_source == output_dir → collecting in place ─────
def test_linux_build_artifact_source_equals_output_dir(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Lines 365, 370-371: when artifact_source is the output_dir itself, the
'collecting in place' branch runs an existence/non-empty check."""
monkeypatch.setattr(build_module, "SshTransport", _FakeTransport)
output_dir = "/opt/ci/output"
build_module._linux_build(
ip_address="10.0.0.1",
ssh_user="ci_build",
key_path=None,
known_hosts=None,
workdir="/opt/ci/build",
output_dir=output_dir,
host_source_dir=None,
clone_url="https://example/repo.git",
clone_branch="main",
clone_commit="",
clone_submodules=False,
build_command="true",
# Use the absolute output dir so srcpath == out
artifact_source=output_dir,
extra_env={},
skip_artifact=False,
)
instance = _FakeTransport.instances[0]
cmds = [c[0] for c in instance.runs]
# The 'collecting in place' guard command must appear.
assert any("if [ ! -d" in c and output_dir in c for c in cmds)
# The wipe-and-copy command must NOT appear.
assert not any("rm -rf" in c and "mkdir -p" in c and output_dir in c for c in cmds)
# ── build run CLI: ssh_key_path from config ────────────────────────────────
def _make_config(ssh_key_path: Path | None = None) -> Config:
paths = Paths(
root=Path("/var/lib/ci"),
templates=Path("/var/lib/ci/templates"),
build_vms=Path("/var/lib/ci/build-vms"),
artifacts=Path("/var/lib/ci/artifacts"),
keys=Path("/var/lib/ci/keys"),
)
return Config(paths=paths, backend=BackendConfig(), ssh_key_path=ssh_key_path)
def test_build_run_linux_ssh_key_from_config(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Line 548: when --ssh-key-path is omitted but config.ssh_key_path is set, it is used."""
_patch(monkeypatch)
key = tmp_path / "ci_linux"
monkeypatch.setattr(build_module, "load_config", lambda: _make_config(ssh_key_path=key))
result = CliRunner().invoke(
cli,
[
"build",
"run",
"--ip-address",
"10.0.0.5",
"--guest-os",
"linux",
"--clone-url",
"https://example/repo.git",
"--build-command",
"true",
"--skip-artifact",
# deliberately NO --ssh-key-path
],
)
assert result.exit_code == 0, result.output
# The transport must have been instantiated with the config key path.
instance = _FakeTransport.instances[0]
assert str(key) in (instance.kwargs.get("key_path") or instance.args[1] if len(instance.args) > 1 else "") or instance.kwargs.get("key_path") == str(key)
# ── build run CLI: linux TransportError → ClickException ──────────────────
def test_build_run_linux_transport_error(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Line 568: TransportError from _linux_build is caught and re-raised as ClickException."""
_patch(monkeypatch)
monkeypatch.setattr(build_module, "load_config", lambda: _make_config())
class _RaisesTransport(_FakeTransport):
def __enter__(self) -> _RaisesTransport:
raise TransportError("connection refused")
monkeypatch.setattr(build_module, "SshTransport", _RaisesTransport)
result = CliRunner().invoke(
cli,
[
"build",
"run",
"--ip-address",
"10.0.0.5",
"--guest-os",
"linux",
"--clone-url",
"https://example/repo.git",
"--build-command",
"true",
"--ssh-key-path",
str(tmp_path / "key"),
],
)
assert result.exit_code != 0
assert "connection refused" in result.output
# ── build run CLI: windows TransportError → ClickException ────────────────
def test_build_run_windows_transport_error(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Lines 591-592: TransportError from _windows_build is caught and re-raised as ClickException."""
_patch(monkeypatch)
monkeypatch.setattr(build_module, "load_config", lambda: _make_config())
class _RaisesTransport(_FakeTransport):
def __enter__(self) -> _RaisesTransport:
raise TransportError("winrm unreachable")
monkeypatch.setattr(build_module, "WinRmTransport", _RaisesTransport)
result = CliRunner().invoke(
cli,
[
"build",
"run",
"--ip-address",
"10.0.0.7",
"--guest-os",
"windows",
"--clone-url",
"https://example/repo.git",
],
)
assert result.exit_code != 0
assert "winrm unreachable" in result.output