Files
AnyPS5/tools/tests/test_pr_overlap.py
T

327 lines
16 KiB
Python

import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import pr_overlap
class OpenPullRequestsTests(unittest.TestCase):
def test_collects_pull_requests_beyond_the_previous_limit(self):
pull_requests = [
{"number": number, "user": {"login": f"author{number}"},
"head": {"sha": f"sha{number}"}, "body": None}
for number in range(1, 202)
]
pull_requests[-1]["body"] = "Depends on: #12\n- #34\n"
def response(*args):
if args[0] == "api":
return json.dumps(pull_requests)
limit = int(args[args.index("-L") + 1])
return json.dumps([
{"number": pr["number"], "author": pr["user"], "headRefOid": pr["head"]["sha"],
"body": pr["body"]} for pr in pull_requests[:limit]
])
with patch.object(pr_overlap, "REPO", "owner/project", create=True), \
patch.object(pr_overlap, "gh", side_effect=response):
found = pr_overlap.open_prs("main")
self.assertEqual(set(found), set(range(1, 202)))
self.assertEqual(found[201], {
"ref": "refs/pr/201", "sha": "sha201", "author": "author201", "depends": {12, 34},
})
self.assertEqual(found[1]["depends"], set())
def test_base_branch_is_encoded_in_the_paginated_request(self):
with patch.object(pr_overlap, "REPO", "owner/project", create=True), \
patch.object(pr_overlap, "gh", return_value="[]") as gh:
self.assertEqual(pr_overlap.open_prs("topic/one&two"), {})
gh.assert_called_once_with(
"api", "--paginate",
"repos/owner/project/pulls?state=open&base=topic%2Fone%26two&per_page=100",
)
def test_single_page_preserves_author_head_and_dependencies(self):
pull_requests = [{"number": 7, "user": {"login": "contributor"}, "head": {"sha": "head-sha"},
"body": "Depends on: none\nUnrelated issue #42"}]
with patch.object(pr_overlap, "REPO", "owner/project", create=True), \
patch.object(pr_overlap, "gh", return_value=json.dumps(pull_requests)):
found = pr_overlap.open_prs("main")
self.assertEqual(found, {
7: {"ref": "refs/pr/7", "sha": "head-sha", "author": "contributor", "depends": set()},
})
def test_collects_concatenated_json_pages_from_paginated_gh_output(self):
pull_requests = [
{"number": number, "user": {"login": f"author{number}"},
"head": {"sha": f"sha{number}"}, "body": None}
for number in range(1, 4)
]
pages = [pull_requests[:2], pull_requests[2:]]
output = "\n".join(json.dumps(page) for page in pages)
with patch.object(pr_overlap, "REPO", "owner/project", create=True), \
patch.object(pr_overlap, "gh", return_value=output):
found = pr_overlap.open_prs("main")
self.assertEqual(set(found), {1, 2, 3})
self.assertEqual(found[3]["sha"], "sha3")
def test_rejects_empty_paginated_output(self):
with patch.object(pr_overlap, "REPO", "owner/project", create=True), \
patch.object(pr_overlap, "gh", return_value=" \n"):
with self.assertRaisesRegex(ValueError, "returned no JSON pages"):
pr_overlap.open_prs("main")
class TopInsertTests(unittest.TestCase):
def test_top_of_file_insertions_clash_on_line_one(self):
pr = {"tree": "t", "files": {"f.cpp"}, "added": set()}
with patch.object(pr_overlap, "git", return_value="@@ -0,0 +1 @@\n+x\n"):
self.assertEqual(pr_overlap.hunks(pr, "f.cpp"), [(1, 1)])
self.assertEqual(pr_overlap.clashes(pr, pr, "f.cpp"), [(1, 1)])
def test_top_of_file_span_links_to_line_one(self):
self.assertEqual(pr_overlap.spans({"f.cpp": [(1, 1)]}, "https://x/blob/sha"),
"`f.cpp` [L1](https://x/blob/sha/f.cpp#L1)")
def test_ordinary_hunk_is_unchanged(self):
pr = {"tree": "t"}
with patch.object(pr_overlap, "git", return_value="@@ -3,2 +3,2 @@\n-a\n-b\n+c\n+d\n"):
self.assertEqual(pr_overlap.hunks(pr, "f.cpp"), [(3, 4)])
class FetchTests(unittest.TestCase):
def test_fetches_each_head_commit_the_api_reported(self):
prs = {
7: {"ref": "refs/pr/7", "sha": "a" * 40, "author": "one", "depends": set()},
9: {"ref": "refs/pr/9", "sha": "b" * 40, "author": "two", "depends": set()},
}
with patch.object(pr_overlap, "REPO", "owner/project", create=True), \
patch.object(pr_overlap, "git", return_value="") as git:
pr_overlap.fetch("main", prs)
git.assert_called_once_with(
"fetch", "--no-tags", "-q", "https://github.com/owner/project.git",
"+refs/heads/main:refs/pr/base", f"+{'a' * 40}:refs/pr/7", f"+{'b' * 40}:refs/pr/9",
)
def test_batches_large_sets_of_pull_request_heads(self):
prs = {
number: {
"ref": f"refs/pr/{number}", "sha": f"{number:040x}",
"author": "contributor", "depends": set(),
}
for number in range(250)
}
with patch.object(pr_overlap, "REPO", "owner/project", create=True), \
patch.object(pr_overlap, "git", return_value="") as git:
pr_overlap.fetch("main", prs)
self.assertEqual(git.call_count, 3)
self.assertEqual([len(call.args) - 4 for call in git.call_args_list], [100, 100, 51])
self.assertEqual(git.call_args_list[0].args[4], "+refs/heads/main:refs/pr/base")
self.assertEqual(git.call_args_list[-1].args[-1], "+00000000000000000000000000000000000000f9:refs/pr/249")
class ConflictTests(unittest.TestCase):
def test_uses_the_shared_base_parent_without_newer_merge_tree_option(self):
first = {"tree": "first-tree", "files": {"shared.cpp"}, "added": set()}
second = {"tree": "second-tree", "files": {"shared.cpp"}, "added": set()}
with patch.object(pr_overlap, "merge", return_value=("merged-tree", set())) as merge:
self.assertEqual(pr_overlap.conflicts(first, second), {})
merge.assert_called_once_with("first-tree", "second-tree")
def real_pull_requests(self, main_changes, older_changes, current_changes):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
root = Path(temporary.name).resolve()
self.assertTrue(root.is_relative_to(Path(tempfile.gettempdir()).resolve()))
self.addCleanup(os.chdir, os.getcwd())
os.chdir(root)
def git(*args):
return subprocess.run(
["git", "-c", "user.name=Overlap Test", "-c", "user.email=test@example.com", *args],
capture_output=True, text=True, encoding="utf-8", check=True).stdout.strip()
git("init", "-q")
git("config", "commit.gpgsign", "false")
for name in ("shared.cpp", "other.cpp", "unrelated.cpp"):
(root / name).write_text("int value = 0;\n", encoding="utf-8")
git("add", ".")
git("commit", "-qm", "test: create base")
base = git("rev-parse", "HEAD")
def branch(name, parent, changes):
git("checkout", "-qb", name, parent)
for path, value in changes.items():
(root / path).write_text(f"int value = {value};\n", encoding="utf-8")
git("add", ".")
git("commit", "-qm", "test: change values", "--allow-empty")
return git("rev-parse", "HEAD")
older = branch("older", base, older_changes)
main = branch("updated-base", base, main_changes)
current = branch("current", main, current_changes)
git("update-ref", "refs/pr/base", main)
prs = {1: {"ref": current, "author": "current-author", "depends": set()},
2: {"ref": older, "author": "older-author", "depends": set()}}
for pr in prs.values():
pr_overlap.scan(pr)
return prs
def test_reports_shared_files_when_older_pr_conflicts_with_main(self):
prs = self.real_pull_requests({"shared.cpp": 2}, {"shared.cpp": 1}, {"shared.cpp": 3})
self.assertIsNone(prs[2]["tree"])
self.assertIsNotNone(prs[1]["tree"])
found = pr_overlap.overlaps(prs, 1, 2)
self.assertIsNotNone(found)
self.assertEqual(found["uncompared"], {"shared.cpp"})
report = pr_overlap.report({2: found}, "https://example.invalid/blob/base")
self.assertIn("could not be completed", report)
self.assertIn("`shared.cpp`", report)
self.assertNotIn("Merging one will break the other", report)
def test_conflict_in_another_file_does_not_hide_shared_changes(self):
prs = self.real_pull_requests({"other.cpp": 2}, {"other.cpp": 1, "shared.cpp": 1}, {"shared.cpp": 3})
self.assertIsNone(prs[2]["tree"])
found = pr_overlap.overlaps(prs, 1, 2)
self.assertIsNotNone(found)
self.assertEqual(found["uncompared"], {"shared.cpp"})
def test_unmergeable_pr_with_no_shared_files_is_not_reported(self):
prs = self.real_pull_requests({"other.cpp": 2}, {"other.cpp": 1}, {"unrelated.cpp": 3})
self.assertIsNone(prs[2]["tree"])
self.assertIsNone(pr_overlap.overlaps(prs, 1, 2))
def test_mergeable_branches_still_report_confirmed_line_conflicts(self):
prs = self.real_pull_requests({}, {"shared.cpp": 1}, {"shared.cpp": 2})
self.assertTrue(all(pr["tree"] for pr in prs.values()))
found = pr_overlap.overlaps(prs, 1, 2)
self.assertEqual(found["conflicts"], {"shared.cpp": [(1, 1)]})
self.assertNotIn("uncompared", found)
def test_mixed_report_keeps_confirmed_and_uncompared_sections(self):
rows = {2: {"added": set(), "conflicts": {"other.cpp": [(1, 1)]}},
3: {"added": set(), "uncompared": {"shared.cpp"}}}
report = pr_overlap.report(rows, "https://example.invalid/blob/base")
self.assertIn("[L1]", report)
self.assertIn("could not be completed", report)
self.assertIn("`shared.cpp`", report)
self.assertTrue(report.startswith(pr_overlap.MARKER))
def test_report_links_pull_requests_without_cross_references(self):
rows = {2: {"added": set(), "conflicts": {"other.cpp": [(1, 1)]}},
3: {"added": set(), "uncompared": {"shared.cpp"}}}
report = pr_overlap.report(rows, "https://github.com/owner/project/blob/base")
self.assertIsNone(re.search(r"#\d|https://github\.com/owner/project/(pull|issues)/", report))
self.assertIn("[PR 2](https://www.github.com/owner/project/pull/2)", report)
self.assertIn("[PR 3](https://www.github.com/owner/project/pull/3)", report)
class UnicodePathTests(unittest.TestCase):
def test_scans_and_reports_conflicts_with_git_path_quoting_enabled(self):
previous = os.getcwd()
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
def git(*args):
return subprocess.run(
["git", "-c", "user.name=Overlap Test", "-c", "user.email=test@example.com",
"-c", "commit.gpgsign=false", *args], cwd=root,
capture_output=True, text=True, encoding="utf-8", check=True).stdout.strip()
filename = "core/libs/prx/libSceExample/caf\u00e9.cpp"
source = root / filename
source.parent.mkdir(parents=True)
source.write_text("int value = 0;\n", encoding="utf-8")
git("init", "-q")
git("config", "core.quotePath", "true")
git("add", ".")
git("commit", "-qm", "test: create base")
base = git("rev-parse", "HEAD")
git("update-ref", "refs/pr/base", base)
for branch, value in (("left", 1), ("right", 2)):
git("checkout", "-qb", branch, base)
source.write_text(f"int value = {value};\n", encoding="utf-8")
git("add", ".")
git("commit", "-qm", "fix: change value")
try:
os.chdir(root)
left, right = {"ref": "left"}, {"ref": "right"}
pr_overlap.scan(left)
pr_overlap.scan(right)
self.assertEqual(left["files"], {filename})
self.assertEqual(right["files"], {filename})
self.assertEqual(pr_overlap.conflicts(left, right), {filename: [(1, 1)]})
finally:
os.chdir(previous)
class CommentStateTests(unittest.TestCase):
def setUp(self):
if os.name == "nt" or not all(shutil.which(tool) for tool in ("bash", "jq", "sed", "sha256sum")):
self.skipTest("the workflow step runs in bash with jq, sed and sha256sum on Linux")
lines = (Path(__file__).resolve().parents[2] / ".github/workflows/pr-overlap.yml").read_text(encoding="utf-8").splitlines()
start = next(index for index, line in enumerate(lines) if "tools/pr_overlap.py" in line)
indent = len(lines[start]) - len(lines[start].lstrip())
while start > 0 and len(lines[start - 1]) - len(lines[start - 1].lstrip()) == indent:
start -= 1
step = []
for line in lines[start:]:
if line.strip() and len(line) - len(line.lstrip()) < indent:
break
step.append(line[indent:])
self.step = "\n".join(step).replace("${{ github.event.repository.default_branch }}", "main")
self.temp = tempfile.TemporaryDirectory()
self.root = Path(self.temp.name)
(self.root / "overlap").mkdir()
def tearDown(self):
self.temp.cleanup()
def report(self, commit, lines):
found = {7: {"conflicts": {"core/a.cpp": [lines]}, "added": set()}}
return pr_overlap.report(found, f"https://github.com/owner/project/blob/{commit}")
def run_step(self, report, comment):
(self.root / "overlap" / "42.md").write_text(report, encoding="utf-8", newline="\n")
(self.root / "comment").write_text(json.dumps([5, comment]) + "\n" if comment else "", encoding="utf-8", newline="\n")
calls = self.root / "calls"
calls.write_text("", encoding="utf-8")
script = "\n".join([
"set -eo pipefail",
"python3() { :; }",
'gh() { printf \'%s\\n\' "$*" >> "$RUNNER_TEMP/calls"; case "$*" in "api repos/"*) cat "$RUNNER_TEMP/comment";; esac; }',
self.step,
])
(self.root / "step.sh").write_text(script, encoding="utf-8", newline="\n")
environment = dict(os.environ, RUNNER_TEMP=str(self.root), GH_REPO="owner/project")
result = subprocess.run(["bash", "step.sh"], cwd=self.root, env=environment, capture_output=True, text=True, timeout=20)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
return [call.split(" ")[:3] for call in calls.read_text(encoding="utf-8").splitlines()]
def test_new_main_commit_alone_makes_no_requests(self):
first = self.report("a" * 40, (3, 4))
self.assertEqual(self.run_step(first, None), [["api", "repos/owner/project/issues/42/comments", "--paginate"], ["pr", "comment", "42"]])
self.assertEqual(self.run_step(self.report("b" * 40, (3, 4)), first), [])
def test_lost_state_reads_the_comment_without_rewriting_it(self):
(self.root / "overlap-state.json").write_text("{}", encoding="utf-8")
self.assertEqual(self.run_step(self.report("b" * 40, (3, 4)), self.report("a" * 40, (3, 4))), [["api", "repos/owner/project/issues/42/comments", "--paginate"]])
def test_changed_lines_still_update_the_comment(self):
first = self.report("a" * 40, (3, 4))
self.run_step(first, None)
self.assertEqual(self.run_step(self.report("b" * 40, (5, 6)), first), [["api", "repos/owner/project/issues/42/comments", "--paginate"], ["api", "-X", "PATCH"]])
if __name__ == "__main__":
unittest.main()