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"