|
| 1 | +# Copyright (c) Microsoft Corporation. |
| 2 | +# Licensed under the MIT license. |
| 3 | + |
| 4 | +import os |
| 5 | +import subprocess |
| 6 | +from types import SimpleNamespace |
| 7 | +from unittest.mock import patch |
| 8 | + |
| 9 | +import pytest |
| 10 | + |
| 11 | +from build_scripts.enforce_alembic_revision_immutability import ( |
| 12 | + _on_release_branch, |
| 13 | + has_revision_violations, |
| 14 | +) |
| 15 | + |
| 16 | +MODIFIED_REVISION = "M\tpyrit/memory/alembic/versions/b2f4c6a8d1e3_add_conversations_table.py" |
| 17 | + |
| 18 | + |
| 19 | +def _completed(stdout: str = "", returncode: int = 0) -> subprocess.CompletedProcess: |
| 20 | + return subprocess.CompletedProcess(args=["git"], returncode=returncode, stdout=stdout, stderr="") |
| 21 | + |
| 22 | + |
| 23 | +@pytest.mark.parametrize( |
| 24 | + "environment, expected", |
| 25 | + [ |
| 26 | + ({"GITHUB_REF": "refs/heads/releases/v1.1.0"}, True), |
| 27 | + ({"GITHUB_REF": "refs/heads/releases/v1.0.1"}, True), |
| 28 | + ({"GITHUB_REF": "refs/heads/main"}, False), |
| 29 | + ({"GITHUB_REF": "refs/heads/releases-notes"}, False), |
| 30 | + ({"GITHUB_REF": "refs/tags/v1.1.0"}, False), |
| 31 | + ({"GITHUB_REF": "refs/pull/42/merge", "GITHUB_BASE_REF": "releases/v1.1.0"}, True), |
| 32 | + ({"GITHUB_REF": "refs/pull/42/merge", "GITHUB_BASE_REF": "main"}, False), |
| 33 | + ({"GITHUB_REF": "refs/heads/gh-readonly-queue/releases/v1.1.0/pr-42-abc123"}, True), |
| 34 | + ({"GITHUB_REF": "refs/heads/gh-readonly-queue/main/pr-42-abc123"}, False), |
| 35 | + ({}, False), |
| 36 | + ], |
| 37 | +) |
| 38 | +def test_on_release_branch_recognizes_release_refs(environment: dict[str, str], expected: bool) -> None: |
| 39 | + """pull_request events carry the target branch in GITHUB_BASE_REF; merge_group embeds it in GITHUB_REF.""" |
| 40 | + with patch.dict("os.environ", environment, clear=True): |
| 41 | + with patch("build_scripts.enforce_alembic_revision_immutability._git_stdout", return_value="main"): |
| 42 | + assert _on_release_branch() is expected |
| 43 | + |
| 44 | + |
| 45 | +@pytest.mark.parametrize( |
| 46 | + "checked_out_branch, expected", |
| 47 | + [("releases/v1.1.0", True), ("main", False), ("HEAD", False)], |
| 48 | +) |
| 49 | +def test_on_release_branch_falls_back_to_checked_out_branch(checked_out_branch: str, expected: bool) -> None: |
| 50 | + """Runs outside GitHub Actions have no ref variables, leaving the branch name as the only signal.""" |
| 51 | + with patch.dict("os.environ", {}, clear=True): |
| 52 | + with patch( |
| 53 | + "build_scripts.enforce_alembic_revision_immutability._git_stdout", |
| 54 | + return_value=checked_out_branch, |
| 55 | + ) as mock_git_stdout: |
| 56 | + assert _on_release_branch() is expected |
| 57 | + |
| 58 | + assert mock_git_stdout.call_args.args == ("rev-parse", "--abbrev-ref", "HEAD") |
| 59 | + |
| 60 | + |
| 61 | +def test_on_release_branch_ignores_branch_name_when_ci_refs_are_present() -> None: |
| 62 | + """A PR from a release-named source branch into main must still be enforced.""" |
| 63 | + environment = {"GITHUB_REF": "refs/pull/42/merge", "GITHUB_BASE_REF": "main"} |
| 64 | + with patch.dict("os.environ", environment, clear=True): |
| 65 | + with patch( |
| 66 | + "build_scripts.enforce_alembic_revision_immutability._git_stdout", |
| 67 | + return_value="releases/v1.1.0", |
| 68 | + ) as mock_git_stdout: |
| 69 | + assert _on_release_branch() is False |
| 70 | + |
| 71 | + mock_git_stdout.assert_not_called() |
| 72 | + |
| 73 | + |
| 74 | +def test_release_branch_push_skips_history_checks() -> None: |
| 75 | + """A release branch push shares neither origin/main nor a comparable previous commit.""" |
| 76 | + |
| 77 | + def _fail_if_called(*args, **kwargs): |
| 78 | + raise AssertionError(f"history check ran on a release branch push: {args}") |
| 79 | + |
| 80 | + with patch.dict(os.environ, {"GITHUB_BASE_REF": ""}, clear=False): |
| 81 | + with patch("build_scripts.enforce_alembic_revision_immutability._on_release_branch", return_value=True): |
| 82 | + with patch("build_scripts.enforce_alembic_revision_immutability._get_violations", return_value=[]): |
| 83 | + with patch("build_scripts.enforce_alembic_revision_immutability._git", side_effect=_fail_if_called): |
| 84 | + assert has_revision_violations() is False |
| 85 | + |
| 86 | + |
| 87 | +def test_release_pull_request_compares_against_its_base() -> None: |
| 88 | + """A pull request into a release branch is comparable against that branch.""" |
| 89 | + calls: list[tuple] = [] |
| 90 | + |
| 91 | + def _record(*args, **kwargs): |
| 92 | + calls.append(args) |
| 93 | + return SimpleNamespace(returncode=0, stdout="", stderr="") |
| 94 | + |
| 95 | + with patch.dict(os.environ, {"GITHUB_BASE_REF": "releases/v1.1.0"}, clear=False): |
| 96 | + with patch("build_scripts.enforce_alembic_revision_immutability._on_release_branch", return_value=True): |
| 97 | + with patch("build_scripts.enforce_alembic_revision_immutability._get_violations", return_value=[]): |
| 98 | + with patch("build_scripts.enforce_alembic_revision_immutability._git", side_effect=_record): |
| 99 | + assert has_revision_violations() is False |
| 100 | + |
| 101 | + assert any("origin/releases/v1.1.0...HEAD" in call for call in calls) |
| 102 | + assert not any("origin/main...HEAD" in call for call in calls) |
| 103 | + |
| 104 | + |
| 105 | +def test_release_pull_request_reports_modified_revision() -> None: |
| 106 | + """The base comparison still catches a revision the pull request itself edits.""" |
| 107 | + |
| 108 | + def _modified(*args, **kwargs): |
| 109 | + if "diff" in args and any(arg == "origin/releases/v1.1.0...HEAD" for arg in args): |
| 110 | + return SimpleNamespace(returncode=0, stdout=f"M\t{MODIFIED_REVISION}\n", stderr="") |
| 111 | + return SimpleNamespace(returncode=0, stdout="", stderr="") |
| 112 | + |
| 113 | + with patch.dict(os.environ, {"GITHUB_BASE_REF": "releases/v1.1.0"}, clear=False): |
| 114 | + with patch("build_scripts.enforce_alembic_revision_immutability._on_release_branch", return_value=True): |
| 115 | + with patch("build_scripts.enforce_alembic_revision_immutability._get_violations", return_value=[]): |
| 116 | + with patch("build_scripts.enforce_alembic_revision_immutability._git", side_effect=_modified): |
| 117 | + assert has_revision_violations() is True |
| 118 | + |
| 119 | + |
| 120 | +def test_release_branch_still_reports_staged_violations() -> None: |
| 121 | + """Skipping the history checks must not stop the staged-change check.""" |
| 122 | + with patch("build_scripts.enforce_alembic_revision_immutability._on_release_branch", return_value=True): |
| 123 | + with patch( |
| 124 | + "build_scripts.enforce_alembic_revision_immutability._get_violations", |
| 125 | + return_value=[MODIFIED_REVISION], |
| 126 | + ): |
| 127 | + assert has_revision_violations() is True |
| 128 | + |
| 129 | + |
| 130 | +def test_branch_comparison_still_runs_off_release_branches() -> None: |
| 131 | + """Positive control: the origin/main comparison must keep catching violations everywhere else.""" |
| 132 | + with patch.dict(os.environ, {"GITHUB_BASE_REF": ""}, clear=False): |
| 133 | + with patch("build_scripts.enforce_alembic_revision_immutability._on_release_branch", return_value=False): |
| 134 | + with patch("build_scripts.enforce_alembic_revision_immutability._get_violations", return_value=[]): |
| 135 | + with patch( |
| 136 | + "build_scripts.enforce_alembic_revision_immutability._git", |
| 137 | + return_value=_completed(stdout=MODIFIED_REVISION), |
| 138 | + ) as mock_git: |
| 139 | + assert has_revision_violations() is True |
| 140 | + |
| 141 | + assert mock_git.call_args.args[:3] == ("diff", "--name-status", "origin/main...HEAD") |
| 142 | + |
| 143 | + |
| 144 | +def test_previous_commit_check_still_runs_off_release_branches() -> None: |
| 145 | + """Positive control: the HEAD~1..HEAD check must keep catching violations everywhere else.""" |
| 146 | + |
| 147 | + def _violations_for(diff_spec: list[str]) -> list[str]: |
| 148 | + return [MODIFIED_REVISION] if diff_spec == ["HEAD~1..HEAD"] else [] |
| 149 | + |
| 150 | + with patch("build_scripts.enforce_alembic_revision_immutability._on_release_branch", return_value=False): |
| 151 | + with patch( |
| 152 | + "build_scripts.enforce_alembic_revision_immutability._get_violations", |
| 153 | + side_effect=_violations_for, |
| 154 | + ): |
| 155 | + with patch("build_scripts.enforce_alembic_revision_immutability._git", return_value=_completed()): |
| 156 | + assert has_revision_violations() is True |
| 157 | + |
| 158 | + |
| 159 | +def test_clean_history_off_release_branches_passes() -> None: |
| 160 | + with patch("build_scripts.enforce_alembic_revision_immutability._on_release_branch", return_value=False): |
| 161 | + with patch("build_scripts.enforce_alembic_revision_immutability._get_violations", return_value=[]): |
| 162 | + with patch("build_scripts.enforce_alembic_revision_immutability._git", return_value=_completed()): |
| 163 | + assert has_revision_violations() is False |
0 commit comments