Files
devx/tests/unit/test_validate_commit_msg.py
T
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 60fd11419c
Post-merge / detect-type (push) Failing after 9s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / badges (push) Failing after 25s
feat: extract reusable dev/CI tools from GRM into devx package
Port core modules (config, exceptions, i18n, api_clients, gitea_cli),
14 CI scripts, 6 dev tools, 5 molecule tools, CLI entry point, workflows,
Makefile, tests (784 tests, 100% coverage), and documentation from GRM.

The devx package is published to the Gitea PyPI registry and consumed
by GRM, infra, and other oblachno-oss projects as a pip dependency.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-22 17:01:20 +02:00

177 lines
7.3 KiB
Python

"""Unit tests for scripts/ci/validate_commit_msg.py."""
import os
import subprocess
import tempfile
from unittest.mock import patch
from click.testing import CliRunner
from devx.ci.validate_commit_msg import first_line, get_branch, main
from devx.config import CONVENTIONAL_RE, TASK_ID_RE
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")
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("DEVX-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("DEVX-19: feat: something")
assert TASK_ID_RE.match("DEVX-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("DEVX-19: feat: add feature")
with patch("devx.ci.validate_commit_msg.get_branch", return_value="DEVX-19"):
runner = CliRunner()
result = runner.invoke(main, [msg_path])
assert result.exit_code == 1
assert "task ID" in result.output
def test_accepts_conventional_on_feature_branch(self) -> None:
msg_path = self._write_msg("feat: add feature")
with patch("devx.ci.validate_commit_msg.get_branch", return_value="DEVX-19"):
runner = CliRunner()
result = runner.invoke(main, [msg_path])
assert result.exit_code == 0
def test_accepts_valid_master_commit(self) -> None:
msg_path = self._write_msg("DEVX-19: feat: add feature")
with patch("devx.ci.validate_commit_msg.get_branch", return_value="master"):
runner = CliRunner()
result = runner.invoke(main, [msg_path])
assert result.exit_code == 0
def test_rejects_master_without_task_id(self) -> None:
msg_path = self._write_msg("feat: add feature")
with patch("devx.ci.validate_commit_msg.get_branch", return_value="master"):
runner = CliRunner()
result = runner.invoke(main, [msg_path])
assert result.exit_code == 1
assert "task ID" in result.output
def test_rejects_master_with_non_conventional_after_task_id(self) -> None:
msg_path = self._write_msg("DEVX-19: random message")
with patch("devx.ci.validate_commit_msg.get_branch", return_value="master"):
runner = CliRunner()
result = runner.invoke(main, [msg_path])
assert result.exit_code == 1
assert "conventional" in result.output
def test_rejects_non_conventional_on_feature_branch(self) -> None:
msg_path = self._write_msg("random message")
with patch("devx.ci.validate_commit_msg.get_branch", return_value="feature"):
runner = CliRunner()
result = runner.invoke(main, [msg_path])
assert result.exit_code == 1
assert "conventional" in result.output
def test_accepts_multiline_conventional(self) -> None:
msg_path = self._write_msg("feat: add feature\n\nBody text.\nMore text.")
with patch("devx.ci.validate_commit_msg.get_branch", return_value="feature"):
runner = CliRunner()
result = runner.invoke(main, [msg_path])
assert result.exit_code == 0
def test_usage_message_without_args(self) -> None:
runner = CliRunner()
result = runner.invoke(main, [])
assert result.exit_code == 2
def test_branch_override_accepts_master_commit(self) -> None:
"""--branch master overrides branch detection (for CI use)."""
msg_path = self._write_msg("DEVX-19: feat: add feature")
runner = CliRunner()
result = runner.invoke(main, [msg_path, "--branch", "master"])
assert result.exit_code == 0
def test_branch_override_rejects_missing_task_id(self) -> None:
"""--branch master still enforces DEVX-N: prefix."""
msg_path = self._write_msg("feat: add feature")
runner = CliRunner()
result = runner.invoke(main, [msg_path, "--branch", "master"])
assert result.exit_code == 1
assert "task ID" in result.output
def test_branch_override_feature_accepts_conventional(self) -> None:
"""--branch feature still rejects DEVX-N prefix."""
msg_path = self._write_msg("DEVX-19: feat: add feature")
runner = CliRunner()
result = runner.invoke(main, [msg_path, "--branch", "feature"])
assert result.exit_code == 1
assert "task ID" in result.output
def test_main_module_block() -> None:
import tempfile
with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
f.write("DEVX-1: feat: test")
msg_path = f.name
with patch("devx.ci.validate_commit_msg.get_branch", return_value="master"):
import devx.ci.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)
# exec() redefines get_branch() from source, overwriting the mock.
# Restore the patched mock so main() uses it.
namespace["get_branch"] = vcm.get_branch
namespace["main"]([msg_path], standalone_mode=False)
os.unlink(msg_path)