Files
solvecraft/scripts/ci/test_main_merge.py
jyviko c41845e6d7 CI: carry only from this pull request's runs, compare patches verbatim
From the review: git patch-id --stable ignores whitespace, so a conflict resolution that only
respaced the change (inside a string, or Python indentation) counted as no change and was
carried; --verbatim compares every byte. And a passing run is now taken only from this pull
request (by number, or for a fork, where GitHub lists none on the run, by head repository and
branch), not from another pull request with the same head commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
2026-10-19 16:42:11 -04:00

148 lines
6.5 KiB
Python

#!/usr/bin/env python3
"""Self-test for main_merge.py. Run: python3 -I scripts/ci/test_main_merge.py (from the repo root).
Builds small throwaway git repositories; needs only git.
"""
import os
import subprocess
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import main_merge # noqa: E402
class Repo:
def __init__(self, root):
self.root = root
self.git("init", "-q", "-b", "main")
self.git("config", "user.email", "t@example.com")
self.git("config", "user.name", "t")
self.git("config", "commit.gpgsign", "false")
def git(self, *args):
return subprocess.run(["git", *args], cwd=self.root, capture_output=True, text=True, check=True).stdout.strip()
def write(self, path, text):
with open(os.path.join(self.root, path), "w") as f:
f.write(text)
def commit(self, msg, **files):
for path, text in files.items():
self.write(path, text)
self.git("add", "-A")
self.git("commit", "-q", "-m", msg)
return self.git("rev-parse", "HEAD")
LINES = "".join(f"line {i}\n" for i in range(40))
class SamePullRequest(unittest.TestCase):
def test_listed_pull_request_must_match(self):
self.assertTrue(main_merge.same_pull_request({"pull_requests": [{"number": 7}]}, 7, "x/y", "b"))
self.assertFalse(main_merge.same_pull_request({"pull_requests": [{"number": 8}]}, 7, "x/y", "b"))
def test_fork_run_matches_by_head_repository_and_branch(self):
run = {"pull_requests": [], "head_repository": {"full_name": "fork/solidcraft"}, "head_branch": "fix"}
self.assertTrue(main_merge.same_pull_request(run, 7, "fork/solidcraft", "fix"))
self.assertFalse(main_merge.same_pull_request(run, 7, "fork/solidcraft", "other"))
self.assertFalse(main_merge.same_pull_request(run, 7, "someone/solidcraft", "fix"))
class MergeOfMain(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.r = Repo(self.tmp.name)
self.r.commit("base", **{"a.txt": LINES, "b.txt": LINES})
self.r.git("checkout", "-q", "-b", "pr")
self.change = self.r.commit("pr change", **{"a.txt": LINES.replace("line 5\n", "line five\n")})
self.cwd = os.getcwd()
os.chdir(self.tmp.name)
def tearDown(self):
os.chdir(self.cwd)
self.tmp.cleanup()
def main_commit(self, **files):
self.r.git("checkout", "-q", "main")
sha = self.r.commit("main moves", **files)
self.r.git("checkout", "-q", "pr")
return sha
def merge_main(self):
self.r.git("merge", "-q", "--no-edit", "main")
return self.r.git("rev-parse", "HEAD")
def test_clean_merge_of_unrelated_main_change_is_carried(self):
base = self.main_commit(**{"b.txt": LINES.replace("line 30\n", "line thirty\n")})
head = self.merge_main()
self.assertEqual(main_merge.merge_of_main(base, head), self.change)
def test_consecutive_merges_of_main_chain_back_to_the_change(self):
self.main_commit(**{"b.txt": LINES.replace("line 30\n", "line thirty\n")})
first = self.merge_main()
base = self.main_commit(**{"b.txt": LINES.replace("line 30\n", "line thirty\n").replace("line 35\n", "line 35b\n")})
head = self.merge_main()
self.assertEqual(main_merge.merge_chain(base, head), [first, self.change])
def test_chain_stops_at_an_ordinary_commit(self):
self.main_commit(**{"b.txt": LINES.replace("line 30\n", "line thirty\n")})
self.merge_main()
later = self.r.commit("more work", **{"a.txt": LINES.replace("line 5\n", "line five\n").replace("line 20\n", "line twenty\n")})
base = self.main_commit(**{"b.txt": LINES.replace("line 30\n", "line thirty\n").replace("line 35\n", "line 35b\n")})
head = self.merge_main()
self.assertEqual(main_merge.merge_chain(base, head), [later])
def test_conflict_resolution_is_not_carried(self):
base = self.main_commit(**{"a.txt": LINES.replace("line 5\n", "line 5 on main\n")})
subprocess.run(["git", "merge", "-q", "main"], cwd=self.tmp.name, capture_output=True)
self.r.write("a.txt", LINES.replace("line 5\n", "line five on both\n"))
self.r.git("add", "-A")
self.r.git("commit", "-q", "--no-edit")
head = self.r.git("rev-parse", "HEAD")
self.assertIsNone(main_merge.merge_of_main(base, head))
def test_main_change_next_to_the_change_is_not_carried(self):
# Main edits a context line of the pull request's hunk: the change's patch differs.
base = self.main_commit(**{"a.txt": LINES.replace("line 7\n", "line seven\n")})
head = self.merge_main()
self.assertIsNone(main_merge.merge_of_main(base, head))
def test_whitespace_change_in_the_change_is_not_carried(self):
# A conflict resolution that only respaces the change's own line still changes the change.
base = self.main_commit(**{"a.txt": LINES.replace("line 5\n", "line 5 on main\n")})
subprocess.run(["git", "merge", "-q", "main"], cwd=self.tmp.name, capture_output=True)
self.r.write("a.txt", LINES.replace("line 5\n", "line five\n"))
self.r.git("add", "-A")
self.r.git("commit", "-q", "--no-edit")
head = self.r.git("rev-parse", "HEAD")
self.assertIsNone(main_merge.merge_of_main(base, head))
def test_ordinary_commit_is_not_carried(self):
head = self.r.commit("more", **{"a.txt": LINES.replace("line 5\n", "line 5b\n")})
base = self.r.git("rev-parse", "main")
self.assertIsNone(main_merge.merge_of_main(base, head))
def test_merge_of_another_branch_is_not_carried(self):
base = self.r.git("rev-parse", "main")
self.r.git("checkout", "-q", "-b", "other", "main")
self.r.commit("other work", **{"c.txt": "x\n"})
self.r.git("checkout", "-q", "pr")
self.r.git("merge", "-q", "--no-edit", "other")
head = self.r.git("rev-parse", "HEAD")
self.assertIsNone(main_merge.merge_of_main(base, head))
def test_change_with_empty_diff_is_not_carried(self):
self.r.git("checkout", "-q", "-b", "empty", "main")
base = self.main_commit(**{"b.txt": LINES.replace("line 30\n", "line thirty\n")})
self.r.git("checkout", "-q", "empty")
self.r.git("merge", "-q", "--no-ff", "--no-edit", "main")
head = self.r.git("rev-parse", "HEAD")
self.assertIsNone(main_merge.merge_of_main(base, head))
if __name__ == "__main__":
unittest.main()