123 lines
3.8 KiB
Python
123 lines
3.8 KiB
Python
import asyncio
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from agentci.adapters.development import (
|
|
DevelopmentEnvironment,
|
|
DevelopmentEnvironmentError,
|
|
)
|
|
|
|
|
|
def 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 script(path: Path, body: str) -> None:
|
|
path.write_text(f"#!/bin/sh\nset -eu\n{body}\n")
|
|
path.chmod(0o755)
|
|
|
|
|
|
async def test_runs_custom_scripts_in_order_with_sanitized_environment(
|
|
tmp_path, monkeypatch
|
|
) -> None:
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
development = environment(tmp_path, ["first", "second"])
|
|
script(
|
|
development.scripts_dir / "first",
|
|
"printf 'first:%s:%s\\n' \"$DEV_TOOLS_DIR\" \"${AGENTCI_SECRET-unset}\" >> order",
|
|
)
|
|
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_supplied_script_names_use_the_same_directory(tmp_path) -> None:
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
development = environment(tmp_path, ["python"])
|
|
script(development.scripts_dir / "python", "printf 'python\\n' > selected")
|
|
|
|
await development.prepare(workspace)
|
|
|
|
assert (workspace / "selected").read_text() == "python\n"
|
|
|
|
|
|
async def test_runs_non_executable_shell_script_from_bind_mount(tmp_path) -> None:
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
development = environment(tmp_path, ["mounted"])
|
|
mounted = development.scripts_dir / "mounted"
|
|
mounted.write_text("#!/bin/sh\nprintf 'mounted\\n' > selected\n")
|
|
mounted.chmod(0o644)
|
|
|
|
await development.prepare(workspace)
|
|
|
|
assert (workspace / "selected").read_text() == "mounted\n"
|
|
|
|
|
|
async def test_serializes_concurrent_preparation(tmp_path, monkeypatch) -> None:
|
|
development = environment(tmp_path, ["shared"])
|
|
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_script_failure_output(tmp_path) -> None:
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
development = environment(tmp_path, ["broken"])
|
|
script(development.scripts_dir / "broken", "printf 'failed detail' >&2; exit 7")
|
|
|
|
with pytest.raises(DevelopmentEnvironmentError, match="exited with 7: failed detail"):
|
|
await development.prepare(workspace)
|
|
|
|
|
|
async def test_times_out_install_script(tmp_path) -> None:
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
development = environment(tmp_path, ["slow"], timeout_seconds=1)
|
|
script(development.scripts_dir / "slow", "exec sleep 10")
|
|
|
|
with pytest.raises(DevelopmentEnvironmentError, match="exceeded 1 seconds"):
|
|
await development.prepare(workspace)
|