Files
grm/tests/unit/test_validate_commit_msg.py
T
Emil Simeonov 4da724ce53 GRM-21: Implement commit validation, CI/CD workflows and repo automation
- 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
2026-06-19 04:27:43 +02:00

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"])