Public Access
Add validate_spec, check_pr_size, fast_molecule, nightly_gate, and create_dependency_pr CI modules. Remove the monolithic pr_review module and its tests. Replace pr_review CI steps with validate_spec + check_pr_size + curl-based APPROVE. Add spec-driven-development and pr-review skills. Remove dead get_reviewer_token. Update translations and AGENTS.md. Closes DEVX-155 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
133 lines
4.8 KiB
Python
133 lines
4.8 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,
|
|
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
|