diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 279006dba..e1497d7f7 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -115,14 +115,26 @@ def conflicts(a, b): return {path: [] if path in a["added"] else clashes(a, b, path) for path in files} +def dependencies(prs, number): + found, pending = set(), [number] + while pending: + for n in prs[pending.pop()]["depends"]: + if n in prs and n not in found: + found.add(n) + pending.append(n) + return found + + def overlaps(prs, a, b): pa, pb = prs[a], prs[b] - if b in pa["depends"] or a in pb["depends"]: + da, db = dependencies(prs, a), dependencies(prs, b) + if b in da or a in db: return None - found = {"added": pa["added"] & pb["added"]} + shared = [prs[n] for n in da & db] + found = {"added": (pa["added"] & pb["added"]).difference(*(pr["added"] for pr in shared))} if pa["author"] != pb["author"]: found["conflicts"] = conflicts(pa, pb) - found["exports"] = pa["exports"] & pb["exports"] + found["exports"] = (pa["exports"] & pb["exports"]).difference(*(pr["exports"] for pr in shared)) return found if any(found.values()) else None diff --git a/tools/tests/test_pr_overlap_dependencies.py b/tools/tests/test_pr_overlap_dependencies.py index f93bb828c..b8590858f 100644 --- a/tools/tests/test_pr_overlap_dependencies.py +++ b/tools/tests/test_pr_overlap_dependencies.py @@ -16,6 +16,23 @@ def pull_requests(body): return pr_overlap.open_prs("main") +STACK = { + 1: (set(), {"one.cpp"}, {"sceFooInit"}), + 2: ({1, 999}, {"one.cpp", "two.cpp"}, {"sceFooInit"}), + 3: ({2}, {"one.cpp", "two.cpp", "three.cpp"}, {"sceFooInit"}), + 4: ({1}, {"one.cpp", "four.cpp"}, {"sceFooInit", "sceFooTerm"}), + 5: ({1}, {"one.cpp", "four.cpp"}, {"sceFooInit", "sceFooTerm"}), + 6: (set(), {"one.cpp"}, {"sceFooInit"}), + 7: ({8}, {"one.cpp", "seven.cpp"}, {"sceFooInit"}), + 8: ({7, 1}, {"one.cpp", "seven.cpp"}, {"sceFooInit"}), +} + + +def stacked_pull_requests(): + return {number: {"author": f"author{number}", "depends": depends, "added": added, "exports": exports, "tree": None} + for number, (depends, added, exports) in STACK.items()} + + class DependencyTests(unittest.TestCase): def test_documented_form_without_colon(self): for body in ("Depends on #2.", "Depends on #2, #3.", "Depends on\t#2 and #3.", @@ -95,5 +112,30 @@ class DependencyTests(unittest.TestCase): self.assertEqual(pr_overlap.overlaps(prs, 1, 2), {"added": {"shared.cpp"}}) +class StackedDependencyTests(unittest.TestCase): + def test_transitive_dependency_suppresses_overlap(self): + prs = stacked_pull_requests() + self.assertIsNone(pr_overlap.overlaps(prs, 1, 3)) + self.assertIsNone(pr_overlap.overlaps(prs, 3, 1)) + + def test_changes_inherited_from_a_shared_dependency_are_not_overlap(self): + prs = stacked_pull_requests() + self.assertIsNone(pr_overlap.overlaps(prs, 3, 4)) + self.assertIsNone(pr_overlap.overlaps(prs, 2, 4)) + + def test_overlap_beyond_a_shared_dependency_is_kept(self): + self.assertEqual(pr_overlap.overlaps(stacked_pull_requests(), 4, 5), + {"added": {"four.cpp"}, "conflicts": {}, "exports": {"sceFooTerm"}}) + + def test_independent_pull_request_keeps_inherited_overlap(self): + self.assertEqual(pr_overlap.overlaps(stacked_pull_requests(), 3, 6), + {"added": {"one.cpp"}, "conflicts": {}, "exports": {"sceFooInit"}}) + + def test_dependency_cycle_terminates(self): + prs = stacked_pull_requests() + self.assertIsNone(pr_overlap.overlaps(prs, 7, 8)) + self.assertIsNone(pr_overlap.overlaps(prs, 8, 2)) + + if __name__ == "__main__": unittest.main()