Hook scripts in .slam/hooks/ are executed at each stage of the build pipeline. If a script exists and is executable, it runs; a non-zero exit stops the build. Available hooks: pre_format / post_format pre_install / post_install pre_test / post_test pre_build / post_build The init command now creates .slam/hooks/ with a README documenting usage and examples, and includes a default pre_format hook that initializes any git submodules that are present.
134 lines
4.0 KiB
Python
134 lines
4.0 KiB
Python
import subprocess
|
|
from dataclasses import dataclass
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from poetry_slam.build_tool import BuildError, BuildTool
|
|
|
|
|
|
def result_factory(out: bytes = b"", err: bytes = b"", code: int = 0):
|
|
@dataclass
|
|
class returnval:
|
|
stdout: bytes = out
|
|
stderr: bytes = err
|
|
returncode: int = code
|
|
|
|
return MagicMock(spec=subprocess, **{"run.return_value": returnval()})
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"out, err, returncode, exception_type",
|
|
[
|
|
(b"", b"", 0, None),
|
|
(b"", b"some error", 1, BuildError),
|
|
],
|
|
)
|
|
def test_slam(monkeypatch, out, err, returncode, exception_type):
|
|
monkeypatch.setattr("poetry_slam.build_tool.subprocess", result_factory(out, err, returncode))
|
|
try:
|
|
assert BuildTool().build() == returncode
|
|
except exception_type:
|
|
pass
|
|
|
|
|
|
def test_format(monkeypatch):
|
|
monkeypatch.setattr("poetry_slam.build_tool.subprocess", result_factory())
|
|
assert BuildTool().auto_format() == 0
|
|
|
|
|
|
def test_install(monkeypatch):
|
|
monkeypatch.setattr("poetry_slam.build_tool.subprocess", result_factory())
|
|
assert BuildTool().install() == 0
|
|
|
|
|
|
def test_test(monkeypatch):
|
|
monkeypatch.setattr("poetry_slam.build_tool.subprocess", result_factory(out=b"out", err=b"err", code=0))
|
|
assert BuildTool(verbose=True).test([]) == 0
|
|
|
|
|
|
def test_run_hook_executes_script(monkeypatch, tmp_path):
|
|
"""Hook script exists and is executable — it should be run."""
|
|
hooks_dir = tmp_path / ".slam" / "hooks"
|
|
hooks_dir.mkdir(parents=True)
|
|
hook = hooks_dir / "pre_build"
|
|
hook.write_text("#!/bin/sh\nexit 0\n")
|
|
hook.chmod(0o755)
|
|
|
|
mock_sub = result_factory()
|
|
monkeypatch.setattr("poetry_slam.build_tool.subprocess", mock_sub)
|
|
bt = BuildTool(project_root=tmp_path)
|
|
bt.run_hook("pre_build")
|
|
mock_sub.run.assert_called_once()
|
|
|
|
|
|
def test_run_hook_skips_missing(monkeypatch, tmp_path):
|
|
"""No hook script — run_hook should do nothing."""
|
|
mock_sub = result_factory()
|
|
monkeypatch.setattr("poetry_slam.build_tool.subprocess", mock_sub)
|
|
bt = BuildTool(project_root=tmp_path)
|
|
bt.run_hook("pre_build")
|
|
mock_sub.run.assert_not_called()
|
|
|
|
|
|
def test_run_hook_non_executable_raises(monkeypatch, tmp_path):
|
|
"""Hook script exists but is not executable — should raise BuildError."""
|
|
hooks_dir = tmp_path / ".slam" / "hooks"
|
|
hooks_dir.mkdir(parents=True)
|
|
hook = hooks_dir / "pre_build"
|
|
hook.write_text("#!/bin/sh\nexit 0\n")
|
|
hook.chmod(0o644)
|
|
|
|
monkeypatch.setattr("poetry_slam.build_tool.subprocess", result_factory(err=b"permission denied", code=126))
|
|
bt = BuildTool(project_root=tmp_path)
|
|
with pytest.raises(BuildError):
|
|
bt.run_hook("pre_build")
|
|
|
|
|
|
def test_run_hook_raises_on_failure(monkeypatch, tmp_path):
|
|
"""Hook script fails — should raise BuildError."""
|
|
hooks_dir = tmp_path / ".slam" / "hooks"
|
|
hooks_dir.mkdir(parents=True)
|
|
hook = hooks_dir / "pre_build"
|
|
hook.write_text("#!/bin/sh\nexit 1\n")
|
|
hook.chmod(0o755)
|
|
|
|
monkeypatch.setattr("poetry_slam.build_tool.subprocess", result_factory(err=b"hook failed", code=1))
|
|
bt = BuildTool(project_root=tmp_path)
|
|
with pytest.raises(BuildError):
|
|
bt.run_hook("pre_build")
|
|
|
|
|
|
def test_build_runs_hooks(monkeypatch, tmp_path):
|
|
"""Build pipeline should call run_hook for each stage."""
|
|
mock_sub = result_factory()
|
|
monkeypatch.setattr("poetry_slam.build_tool.subprocess", mock_sub)
|
|
bt = BuildTool(project_root=tmp_path)
|
|
|
|
hook_calls = []
|
|
original_run_hook = bt.run_hook
|
|
|
|
def tracking_run_hook(stage):
|
|
hook_calls.append(stage)
|
|
original_run_hook(stage)
|
|
|
|
bt.run_hook = tracking_run_hook
|
|
bt.build()
|
|
|
|
assert hook_calls == [
|
|
"pre_format",
|
|
"post_format",
|
|
"pre_install",
|
|
"post_install",
|
|
"pre_test",
|
|
"post_test",
|
|
"pre_build",
|
|
"post_build",
|
|
]
|
|
|
|
|
|
def test_hooks_dir_property(tmp_path):
|
|
"""hooks_dir should point to .slam/hooks/ under project_root."""
|
|
bt = BuildTool(project_root=tmp_path)
|
|
assert bt.hooks_dir == tmp_path / ".slam" / "hooks"
|