Files
agentci/tests/test_development.py
T
StanPonomarev ce9f1e3d20
Publish container image / Build and push (push) Successful in 32s
refactor tests
2026-07-26 23:49:40 +02:00

183 lines
5.6 KiB
Python

import asyncio
from pathlib import Path
import pytest
from agentci.integrations.development import (
DevelopmentEnvironment,
DevelopmentEnvironmentError,
)
def development_environment(
tmp_path: Path, scripts: list[str], *, timeout_seconds: int = 5
) -> DevelopmentEnvironment:
scripts_dir = tmp_path / "scripts"
scripts_dir.mkdir()
return DevelopmentEnvironment(
scripts=scripts,
scripts_dir=scripts_dir,
tools_dir=tmp_path / "tools",
timeout_seconds=timeout_seconds,
python_version="3.13",
dotnet_channel="10.0",
)
def shell_script(path: Path, body: str, *, executable: bool = True) -> None:
path.write_text(f"#!/bin/sh\nset -eu\n{body}\n")
path.chmod(0o755 if executable else 0o644)
@pytest.fixture
def workspace(tmp_path: Path) -> Path:
path = tmp_path / "workspace"
path.mkdir()
return path
async def test_runs_custom_scripts_in_order_with_sanitized_environment(
tmp_path: Path,
workspace: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
development = development_environment(tmp_path, ["first", "second"])
shell_script(
development.scripts_dir / "first",
'printf \'first:%s:%s\\n\' "$DEV_TOOLS_DIR" "${AGENTCI_SECRET-unset}" >> order',
)
shell_script(development.scripts_dir / "second", "printf 'second\\n' >> order")
monkeypatch.setenv("AGENTCI_SECRET", "must-not-leak")
await development.prepare(workspace)
assert (workspace / "order").read_text().splitlines() == [
f"first:{tmp_path / 'tools'}:unset",
"second",
]
assert (tmp_path / "tools" / "bin").is_dir()
async def test_runs_non_executable_shell_script_from_bind_mount(
tmp_path: Path,
workspace: Path,
) -> None:
development = development_environment(tmp_path, ["mounted"])
shell_script(
development.scripts_dir / "mounted",
"printf 'mounted\\n' > selected",
executable=False,
)
await development.prepare(workspace)
assert (workspace / "selected").read_text() == "mounted\n"
async def test_serializes_concurrent_preparation(tmp_path, monkeypatch) -> None:
development = development_environment(tmp_path, ["shared"])
shell_script(development.scripts_dir / "shared", "true")
started = asyncio.Event()
release = asyncio.Event()
active = 0
maximum_active = 0
async def run(*_args) -> None:
nonlocal active, maximum_active
active += 1
maximum_active = max(maximum_active, active)
started.set()
await release.wait()
active -= 1
monkeypatch.setattr(development, "_run", run)
first = asyncio.create_task(development.prepare(tmp_path / "first"))
await started.wait()
second = asyncio.create_task(development.prepare(tmp_path / "second"))
await asyncio.sleep(0)
release.set()
await asyncio.gather(first, second)
assert maximum_active == 1
async def test_reports_missing_install_script(tmp_path: Path, workspace: Path) -> None:
development = development_environment(tmp_path, ["missing"])
with pytest.raises(
DevelopmentEnvironmentError,
match=r"Install script 'missing' was not found",
):
await development.prepare(workspace)
async def test_stops_after_failure_and_reports_output(
tmp_path: Path,
workspace: Path,
) -> None:
development = development_environment(tmp_path, ["first", "second"])
shell_script(
development.scripts_dir / "first",
"printf 'first\\n' > first-ran; printf 'failed detail' >&2; exit 7",
)
shell_script(development.scripts_dir / "second", "printf 'second\\n' > second-ran")
with pytest.raises(DevelopmentEnvironmentError, match="exited with 7: failed detail"):
await development.prepare(workspace)
assert (workspace / "first-ran").read_text() == "first\n"
assert not (workspace / "second-ran").exists()
async def test_failure_output_keeps_only_bounded_tail(
tmp_path: Path,
workspace: Path,
) -> None:
development = development_environment(tmp_path, ["verbose"])
shell_script(
development.scripts_dir / "verbose",
"printf 'discarded-prefix' >&2; "
'i=0; while [ "$i" -lt 2100 ]; do printf x >&2; i=$((i + 1)); done; '
"printf 'useful-tail' >&2; exit 9",
)
with pytest.raises(DevelopmentEnvironmentError) as raised:
await development.prepare(workspace)
message = str(raised.value)
assert "discarded-prefix" not in message
assert message.endswith("useful-tail")
async def test_wraps_subprocess_start_error(
tmp_path: Path,
workspace: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
development = development_environment(tmp_path, ["broken"])
shell_script(development.scripts_dir / "broken", "true")
async def create_subprocess_exec(*_args, **_kwargs):
raise OSError("exec unavailable")
monkeypatch.setattr(
"agentci.integrations.development.asyncio.create_subprocess_exec",
create_subprocess_exec,
)
with pytest.raises(
DevelopmentEnvironmentError,
match="Could not run install script 'broken': exec unavailable",
) as raised:
await development.prepare(workspace)
assert isinstance(raised.value.__cause__, OSError)
async def test_times_out_install_script(tmp_path: Path, workspace: Path) -> None:
development = development_environment(tmp_path, ["slow"], timeout_seconds=1)
shell_script(development.scripts_dir / "slow", "exec sleep 10")
with pytest.raises(DevelopmentEnvironmentError, match="exceeded 1 seconds"):
await development.prepare(workspace)