Public Access
170 lines
6.5 KiB
Python
170 lines
6.5 KiB
Python
"""Unit tests for devx.ci.check_pr_size."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from click.testing import CliRunner
|
|
|
|
from devx.ci.check_pr_size import (
|
|
check_size,
|
|
cli,
|
|
get_diff_stats,
|
|
has_refactoring_label,
|
|
is_excluded,
|
|
)
|
|
|
|
|
|
class TestIsExcluded:
|
|
def test_excludes_changelog(self) -> None:
|
|
assert is_excluded("CHANGELOG.md", ["CHANGELOG.md"])
|
|
|
|
def test_excludes_svg_glob(self) -> None:
|
|
assert is_excluded("docs/badges/coverage.svg", ["*.svg"])
|
|
|
|
def test_does_not_exclude_source(self) -> None:
|
|
assert not is_excluded("src/devx/ci/check_pr_size.py", ["CHANGELOG.md", "*.svg"])
|
|
|
|
def test_excludes_readme(self) -> None:
|
|
assert is_excluded("README.md", ["README.md"])
|
|
|
|
|
|
class TestCheckSize:
|
|
def test_under_limits_passes(self) -> None:
|
|
stats = [("src/main.py", 100, 50), ("tests/test_main.py", 80, 20)]
|
|
ok, detail = check_size(stats, max_lines=500, max_files=10, excluded_patterns=[])
|
|
assert ok is True
|
|
assert "250" in detail # 100+50+80+20
|
|
|
|
def test_over_lines_fails(self) -> None:
|
|
stats = [("src/main.py", 300, 300)]
|
|
ok, detail = check_size(stats, max_lines=500, max_files=10, excluded_patterns=[])
|
|
assert ok is False
|
|
assert "600" in detail
|
|
|
|
def test_over_files_fails(self) -> None:
|
|
stats = [(f"src/file{i}.py", 10, 5) for i in range(15)]
|
|
ok, detail = check_size(stats, max_lines=500, max_files=10, excluded_patterns=[])
|
|
assert ok is False
|
|
assert "15" in detail
|
|
|
|
def test_excluded_files_not_counted(self) -> None:
|
|
stats = [("CHANGELOG.md", 500, 500), ("src/main.py", 10, 5)]
|
|
ok, detail = check_size(stats, max_lines=500, max_files=10, excluded_patterns=["CHANGELOG.md"])
|
|
assert ok is True
|
|
assert "15" in detail # only 10+5
|
|
|
|
def test_empty_stats_passes(self) -> None:
|
|
ok, detail = check_size([], max_lines=500, max_files=10, excluded_patterns=[])
|
|
assert ok is True
|
|
|
|
|
|
class TestGetDiffStats:
|
|
@patch("devx.ci.check_pr_size.subprocess.run")
|
|
def test_parses_numstat_output(self, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(
|
|
returncode=0,
|
|
stdout="10\t5\tsrc/main.py\n20\t10\ttests/test_main.py\n",
|
|
stderr="",
|
|
)
|
|
stats = get_diff_stats("origin/master", "HEAD")
|
|
assert len(stats) == 2
|
|
assert stats[0] == ("src/main.py", 10, 5)
|
|
assert stats[1] == ("tests/test_main.py", 20, 10)
|
|
|
|
@patch("devx.ci.check_pr_size.subprocess.run")
|
|
def test_handles_binary_files(self, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(
|
|
returncode=0,
|
|
stdout="-\t-\timage.png\n",
|
|
stderr="",
|
|
)
|
|
stats = get_diff_stats("origin/master", "HEAD")
|
|
assert len(stats) == 1
|
|
assert stats[0] == ("image.png", 0, 0)
|
|
|
|
@patch("devx.ci.check_pr_size.subprocess.run")
|
|
def test_empty_output(self, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
stats = get_diff_stats("origin/master", "HEAD")
|
|
assert stats == []
|
|
|
|
@patch("devx.ci.check_pr_size.subprocess.run")
|
|
def test_git_diff_failure_raises(self, mock_run: MagicMock) -> None:
|
|
import pytest
|
|
|
|
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="fatal: bad ref")
|
|
with pytest.raises(Exception, match="git diff|bad ref"):
|
|
get_diff_stats("origin/master", "HEAD")
|
|
|
|
@patch("devx.ci.check_pr_size.subprocess.run")
|
|
def test_malformed_line_skipped(self, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(
|
|
returncode=0,
|
|
stdout="not_a_valid_line\n10\t5\tsrc/main.py\n",
|
|
stderr="",
|
|
)
|
|
stats = get_diff_stats("origin/master", "HEAD")
|
|
assert len(stats) == 1
|
|
assert stats[0] == ("src/main.py", 10, 5)
|
|
|
|
|
|
class TestCli:
|
|
@patch("devx.ci.check_pr_size.subprocess.run")
|
|
def test_passes_when_small(self, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(
|
|
returncode=0,
|
|
stdout="10\t5\tsrc/main.py\n",
|
|
stderr="",
|
|
)
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["--base", "origin/master", "--head", "HEAD"])
|
|
assert result.exit_code == 0
|
|
assert "OK" in result.output
|
|
|
|
@patch("devx.ci.check_pr_size.subprocess.run")
|
|
def test_fails_when_too_large(self, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(
|
|
returncode=0,
|
|
stdout="300\t300\tsrc/main.py\n",
|
|
stderr="",
|
|
)
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["--base", "origin/master", "--head", "HEAD", "--max-lines", "500"])
|
|
assert result.exit_code != 0
|
|
assert "600" in result.output
|
|
|
|
@patch("devx.ci.check_pr_size.subprocess.run")
|
|
@patch("devx.ci.check_pr_size.has_refactoring_label", return_value=True)
|
|
def test_bypasses_with_refactoring_label(self, mock_label: MagicMock, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(
|
|
returncode=0,
|
|
stdout="300\t300\tsrc/main.py\n",
|
|
stderr="",
|
|
)
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli,
|
|
["--base", "origin/master", "--head", "HEAD", "--repo", "owner/repo", "--pr-number", "42"],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "bypassed" in result.output.lower()
|
|
|
|
|
|
class TestHasRefactoringLabel:
|
|
@patch("devx.ci.check_pr_size.GiteaClient")
|
|
@patch("devx.ci.check_pr_size.get_ci_token", return_value="fake-token")
|
|
def test_returns_true_when_label_present(self, mock_token: MagicMock, mock_client_cls: MagicMock) -> None:
|
|
mock_client = mock_client_cls.return_value
|
|
mock_client.get_pr.return_value = {"labels": [{"name": "refactoring"}, {"name": "bug"}]}
|
|
assert has_refactoring_label("owner/repo", 42) is True
|
|
|
|
@patch("devx.ci.check_pr_size.GiteaClient")
|
|
@patch("devx.ci.check_pr_size.get_ci_token", return_value="fake-token")
|
|
def test_returns_false_when_label_absent(self, mock_token: MagicMock, mock_client_cls: MagicMock) -> None:
|
|
mock_client = mock_client_cls.return_value
|
|
mock_client.get_pr.return_value = {"labels": [{"name": "bug"}]}
|
|
assert has_refactoring_label("owner/repo", 42) is False
|
|
|
|
@patch("devx.ci.check_pr_size.get_ci_token", side_effect=Exception("no token"))
|
|
def test_returns_false_on_error(self, mock_token: MagicMock) -> None:
|
|
assert has_refactoring_label("owner/repo", 42) is False
|