- Add scripts/validate_commit_msg.py with conventional commit enforcement - Add scripts/configure_repo.py for Gitea branch protection and labels - Add scripts/__init__.py for Python package importability - Create Gitea Actions workflows: ci, auto-merge, post-merge, publish - Update .pre-commit-config.yaml with commit-msg hook - Update pyproject.toml pythonpath and coverage for scripts - Add comprehensive unit tests for both scripts with 100% coverage
119 lines
4.9 KiB
Python
119 lines
4.9 KiB
Python
"""Unit tests for scripts/validate_commit_msg.py."""
|
|
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from scripts.validate_commit_msg import CONVENTIONAL_RE, TASK_ID_RE, first_line, get_branch, main
|
|
|
|
|
|
class TestHelpers:
|
|
def test_first_line_single(self) -> None:
|
|
assert first_line("feat: add something") == "feat: add something"
|
|
|
|
def test_first_line_multiline(self) -> None:
|
|
msg = "feat: add something\n\nBody text here.\nMore body."
|
|
assert first_line(msg) == "feat: add something"
|
|
|
|
def test_conventional_re_matches_valid(self) -> None:
|
|
assert CONVENTIONAL_RE.match("feat: add feature")
|
|
assert CONVENTIONAL_RE.match("fix: bug fix")
|
|
assert CONVENTIONAL_RE.match("chore: update deps")
|
|
assert CONVENTIONAL_RE.match("docs: update readme")
|
|
assert CONVENTIONAL_RE.match("style: format code")
|
|
assert CONVENTIONAL_RE.match("refactor: simplify")
|
|
assert CONVENTIONAL_RE.match("perf: speed up")
|
|
assert CONVENTIONAL_RE.match("test: add tests")
|
|
assert CONVENTIONAL_RE.match("ci: update workflow")
|
|
assert CONVENTIONAL_RE.match("build: update deps")
|
|
assert CONVENTIONAL_RE.match("revert: undo change")
|
|
assert CONVENTIONAL_RE.match("BREAKING CHANGE: major")
|
|
|
|
def test_conventional_re_allows_scope(self) -> None:
|
|
assert CONVENTIONAL_RE.match("feat(cli): add --url option")
|
|
assert CONVENTIONAL_RE.match("fix(api): handle timeout")
|
|
|
|
def test_conventional_re_rejects_invalid(self) -> None:
|
|
assert not CONVENTIONAL_RE.match("GRM-19: feat: something")
|
|
assert not CONVENTIONAL_RE.match("random message")
|
|
assert not CONVENTIONAL_RE.match("feat:")
|
|
assert not CONVENTIONAL_RE.match(": description")
|
|
|
|
def test_task_id_re_matches(self) -> None:
|
|
assert TASK_ID_RE.match("GRM-19: feat: something")
|
|
assert TASK_ID_RE.match("GRM-123: fix: bug")
|
|
|
|
def test_task_id_re_rejects(self) -> None:
|
|
assert not TASK_ID_RE.match("feat: something")
|
|
assert not TASK_ID_RE.match("GRM: something")
|
|
|
|
|
|
class TestGetBranch:
|
|
def test_returns_branch_name(self) -> None:
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value.stdout = "feature-branch\n"
|
|
mock_run.return_value.returncode = 0
|
|
assert get_branch() == "feature-branch"
|
|
|
|
def test_returns_empty_on_error(self) -> None:
|
|
with patch("subprocess.run", side_effect=subprocess.CalledProcessError(1, "git")):
|
|
assert get_branch() == ""
|
|
|
|
|
|
class TestMain:
|
|
def _write_msg(self, content: str) -> str:
|
|
fd, path = tempfile.mkstemp()
|
|
with os.fdopen(fd, "w") as f:
|
|
f.write(content)
|
|
return path
|
|
|
|
def test_rejects_task_id_on_feature_branch(self) -> None:
|
|
msg_path = self._write_msg("GRM-19: feat: add feature")
|
|
with patch("scripts.validate_commit_msg.get_branch", return_value="GRM-19"):
|
|
with pytest.raises(SystemExit) as exc:
|
|
main(["validate_commit_msg.py", msg_path])
|
|
assert exc.value.code == 1
|
|
|
|
def test_accepts_conventional_on_feature_branch(self) -> None:
|
|
msg_path = self._write_msg("feat: add feature")
|
|
with patch("scripts.validate_commit_msg.get_branch", return_value="GRM-19"):
|
|
main(["validate_commit_msg.py", msg_path])
|
|
|
|
def test_accepts_anything_on_master(self) -> None:
|
|
msg_path = self._write_msg("GRM-19: feat: add feature")
|
|
with patch("scripts.validate_commit_msg.get_branch", return_value="master"):
|
|
main(["validate_commit_msg.py", msg_path])
|
|
|
|
def test_rejects_non_conventional_on_feature_branch(self) -> None:
|
|
msg_path = self._write_msg("random message")
|
|
with patch("scripts.validate_commit_msg.get_branch", return_value="feature"):
|
|
with pytest.raises(SystemExit) as exc:
|
|
main(["validate_commit_msg.py", msg_path])
|
|
assert exc.value.code == 1
|
|
|
|
def test_accepts_multiline_conventional(self) -> None:
|
|
msg_path = self._write_msg("feat: add feature\n\nBody text.\nMore text.")
|
|
with patch("scripts.validate_commit_msg.get_branch", return_value="feature"):
|
|
main(["validate_commit_msg.py", msg_path])
|
|
|
|
def test_usage_message_without_args(self) -> None:
|
|
with pytest.raises(SystemExit) as exc:
|
|
main([])
|
|
assert exc.value.code == 1
|
|
|
|
|
|
def test_main_module_block() -> None:
|
|
with patch("scripts.validate_commit_msg.get_branch", return_value="master"):
|
|
import scripts.validate_commit_msg as vcm
|
|
|
|
with open(vcm.__file__) as f:
|
|
source = f.read()
|
|
# Remove __main__ block so exec doesn't call main() before we control sys.argv
|
|
source = source.replace('if __name__ == "__main__":\n main()\n', "")
|
|
namespace = dict(vcm.__dict__)
|
|
exec(compile(source, vcm.__file__, "exec"), namespace)
|
|
namespace["main"](["validate_commit_msg.py", "/dev/null"])
|