diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index c976011d..d8a70667 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -33,10 +33,11 @@ ANON_QUOTA = 60 TRIAGE_PATH_SAMPLE = 5 STATUS_ORDER = ( - "ANCHORED", "EXTERNAL", "SHIFTED", "RENAMED", "AMBIGUOUS", "CHANGED", "GONE", + "ANCHORED", "EXTERNAL", "SHIFTED", "RENAMED", "MOVED", "AMBIGUOUS", + "CHANGED", "GONE", ) REVIEW_STATUSES = ("CHANGED", "GONE", "AMBIGUOUS") -REBASE_STATUSES = ("SHIFTED", "RENAMED") +REBASE_STATUSES = ("SHIFTED", "RENAMED", "MOVED") WIDEN_STEPS = (0, 3, 6, 12, 25, 50) MAX_MATCH_LINES = 20000 @@ -462,6 +463,16 @@ def anchor_part( anchored = retry note = f"cited line was {nudged[0] - part.start:+d} off its subject" status = anchored.status + if status == "CHANGED" and anchored.start is not None and tokens: + # The cited code changed, but the value the entry declares is provably + # inside the new range, so the ref can follow it without a judgement + # call on the edit itself. + block = "\n".join( + head_lines[anchored.start - 1 : (anchored.end or anchored.start)] + ).lower() + if any(token in block for token in tokens): + status = "MOVED" + note = "content edited, declared value present in the new range" if renamed and status in ("ANCHORED", "SHIFTED"): status = "RENAMED" return PartResult( diff --git a/tests/test_profile_sync.py b/tests/test_profile_sync.py index 46e45635..db64e7f7 100644 --- a/tests/test_profile_sync.py +++ b/tests/test_profile_sync.py @@ -641,6 +641,48 @@ class TestAnchorPart(unittest.TestCase): self.assertIn("no rename found", result.reason) +class TestMovedStatus(unittest.TestCase): + """A changed range whose subject is provably inside it can be followed.""" + + def test_declared_value_inside_the_new_range_gives_moved(self): + fetch = make_fetch({ + (PIN, "a.c"): ["x", 'load("bios.bin", 0x1000)'], + (HEAD, "a.c"): ["pad", "pad", 'load("bios.bin", 0x2000, flags)'], + }) + result = anchor_part( + RefPart("a.c", 2, 2, "a.c:2"), + fetch, + renamer(CompareResult([], False)), + None, + ["bios.bin"], + ) + self.assertEqual(result.status, "MOVED") + self.assertEqual(result.start, 3) + self.assertIn("declared value present", result.reason) + + def test_value_absent_from_the_new_range_stays_changed(self): + fetch = make_fetch({ + (PIN, "a.c"): ["x", 'load("bios.bin")'], + (HEAD, "a.c"): ["x", 'load("other.bin")'], + }) + result = anchor_part( + RefPart("a.c", 2, 2, "a.c:2"), + fetch, + renamer(CompareResult([], False)), + None, + ["bios.bin"], + ) + self.assertEqual(result.status, "CHANGED") + + def test_moved_is_mechanically_recalable(self): + self.assertIn("MOVED", profile_sync.REBASE_STATUSES) + self.assertNotIn("MOVED", profile_sync.REVIEW_STATUSES) + + def test_moved_ranks_below_ambiguous(self): + self.assertEqual(worst_status(["MOVED", "AMBIGUOUS"]), "AMBIGUOUS") + self.assertEqual(worst_status(["MOVED", "SHIFTED"]), "MOVED") + + class TestSelectRepo(unittest.TestCase): def test_source_wins_over_upstream(self): repo = select_repo(