9e76f8c4f0
Twice today (runs 40301239508, 40311712302) mob_ai failed in parity-main, passed when bisect ran it again at the same HEAD, and the run still ended red, so no game release could follow main for three hours. The harness step now continues on error and the bisect step gives the verdict: 0 when every failed harness passes again at HEAD, 1 when they fail at the base too or a merge is named. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
74 lines
4.2 KiB
Python
74 lines
4.2 KiB
Python
"""tools/parity_main.py: the base of a parity-main run, and the bisect after a failure."""
|
|
from pathlib import Path
|
|
import tempfile
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
from tools import parity_main
|
|
|
|
|
|
class ParityMainTests(unittest.TestCase):
|
|
def test_first_bad_finds_the_first_failing_merge(self):
|
|
commits = list("abcdefgh") # h failed; the base before a passed
|
|
for culprit in commits:
|
|
tried = []
|
|
fails = lambda commit: tried.append(commit) or commit >= culprit
|
|
self.assertEqual(parity_main.first_bad(commits, fails), culprit)
|
|
self.assertLessEqual(len(tried), 3, "log2 of 8 merges")
|
|
self.assertEqual(parity_main.first_bad(["a"], lambda commit: self.fail("nothing to try")), "a")
|
|
|
|
def test_base_is_the_newest_green_run_head_contains(self):
|
|
with tempfile.TemporaryDirectory() as tmp, mock.patch.object(parity_main, "ROOT", Path(tmp)):
|
|
git = lambda *arguments: parity_main.git(*arguments).stdout.strip()
|
|
git("init", "-q")
|
|
for n in range(3):
|
|
git("-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "--allow-empty", "-m", str(n))
|
|
head, parent, grandparent = git("rev-parse", "HEAD"), git("rev-parse", "HEAD^1"), git("rev-parse", "HEAD~2")
|
|
git("checkout", "-q", "-b", "side", grandparent)
|
|
git("-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "--allow-empty", "-m", "side")
|
|
side = git("rev-parse", "HEAD")
|
|
git("checkout", "-q", head)
|
|
self.assertEqual(parity_main.base(runs=[side, grandparent, parent]), grandparent,
|
|
"a run on a commit HEAD doesn't contain is skipped")
|
|
self.assertEqual(parity_main.base(runs=[]), parent, "no green run yet: the last merge")
|
|
self.assertEqual(parity_main.base(runs=[head]), head)
|
|
|
|
def test_bisect_names_a_merge_only_when_head_fails_and_the_base_passes(self):
|
|
with tempfile.TemporaryDirectory() as tmp, mock.patch.object(parity_main, "ROOT", Path(tmp)):
|
|
git = lambda *arguments: parity_main.git(*arguments).stdout.strip()
|
|
git("init", "-q")
|
|
for n in range(4):
|
|
git("-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "--allow-empty",
|
|
"-m", f"Merge pull request #{n} from x/y")
|
|
commits = git("rev-list", "--reverse", "HEAD").split()
|
|
last_run = Path(tmp) / ".local/parity-runner/last-run.json"
|
|
last_run.parent.mkdir(parents=True)
|
|
last_run.write_text('{"failed": ["music"]}')
|
|
real_run = parity_main.subprocess.run
|
|
for broken_from, blamed, code in ((None, None, 0), (0, None, 1), (2, "2", 1)):
|
|
comments = []
|
|
# git runs for real in the scratch repo; a `gh pr comment` is only recorded, never sent.
|
|
fake_run = lambda command, **kwargs: (comments.append(command) if command[0] == "gh"
|
|
else real_run(command, **kwargs))
|
|
with mock.patch.object(parity_main, "LAST_RUN", last_run), \
|
|
mock.patch.object(parity_main, "run_harnesses", lambda names, timeout: broken_from is not None
|
|
and commits.index(git("rev-parse", "HEAD")) >= broken_from), \
|
|
mock.patch.object(parity_main.subprocess, "run", fake_run), \
|
|
mock.patch("builtins.print") as printed:
|
|
self.assertEqual(parity_main.bisect(commits[0], 60, comment=True), code,
|
|
"0 only when the failures pass again at HEAD")
|
|
if blamed:
|
|
self.assertEqual([command[3] for command in comments], [blamed])
|
|
else:
|
|
self.assertEqual(comments, [], printed.call_args_list[-1])
|
|
self.assertEqual(git("rev-parse", "HEAD"), commits[-1], "HEAD checked out again")
|
|
|
|
def test_a_harness_a_commit_does_not_have_cannot_fail_there(self):
|
|
with mock.patch.object(parity_main.subprocess, "run") as run:
|
|
self.assertFalse(parity_main.run_harnesses(["not_a_harness_yet"], 60))
|
|
run.assert_not_called()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|