mirror of
https://github.com/pytorch/pytorch.git
synced 2026-10-28 18:56:51 +08:00
## Summary - Add an offline check that each `test/cpython/v3_13` adapted test and its `.diff` stay in sync: reverse-apply the diff, match `sha256(pristine)` from `upstream_manifest.json` (tag/path taken from each file header), and assert `apply(pristine, diff) == adapted`. - Add `tools/regenerate_cpython_diffs.py` to fetch upstream sources, regenerate stale diffs, and refresh the manifest. - Regenerate drifted `.diff` files, add the missing source URL on `test_unittest/test_assertions.py`, and fix three headers that pointed at the wrong upstream file (`test_binop`, `test_heapq`, `test_patma`). Fixes #189607 ## Test plan - [x] `python tools/regenerate_cpython_diffs.py --check` - [ ] `python test/dynamo/test_cpython_diff_sync.py CPythonDiffSyncTests` - [ ] Confirm CI runs the new dynamo sync test Pull Request resolved: https://github.com/pytorch/pytorch/pull/191767 Approved by: https://github.com/guilhermeleobas, https://github.com/williamwen42 Co-authored-by: Guilherme Leobas <guilhermeleobas@gmail.com>
281 lines
10 KiB
Python
281 lines
10 KiB
Python
# Owner(s): ["module: dynamo"]
|
|
|
|
"""Unit tests for tools/cpython_diff_sync offline sync helpers.
|
|
|
|
See https://github.com/pytorch/pytorch/issues/189607
|
|
|
|
Lives at test/test_cpython_diff_sync.py (not under test/cpython/) so run_test.py
|
|
does not exclude it via the version-prefix filter, and so it is picked up by
|
|
normal pull shards rather than the Dynamo-wrapped cpython suite.
|
|
|
|
CI enforcement for the full tree also runs via the CPYTHON_DIFF_SYNC lintrunner
|
|
adapter (tools/linter/adapters/cpython_diff_sync_linter.py).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import re
|
|
import shutil
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from torch.testing._internal.common_utils import run_tests, TestCase
|
|
|
|
|
|
def _load_module(name: str, relative: str):
|
|
path = Path(__file__).resolve().parents[1] / relative
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
if spec is None or spec.loader is None:
|
|
raise AssertionError(f"cannot load {path}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
diff_sync = _load_module("torch_cpython_diff_sync", "tools/cpython_diff_sync.py")
|
|
regen = _load_module(
|
|
"torch_regenerate_cpython_diffs", "tools/regenerate_cpython_diffs.py"
|
|
)
|
|
|
|
_FAKE_REL = "test/cpython/v3_13/test_fake.py"
|
|
_FAKE_TAG = "v3.13.5"
|
|
_FAKE_UPSTREAM = "Lib/test/test_fake.py"
|
|
_OUTSIDE_HUNK_LINE = b'@unittest.skip("Dynamo: not supported yet")\n'
|
|
_FAKE_HEADER = b"""\
|
|
# ======= BEGIN Dynamo patch =======
|
|
# https://raw.githubusercontent.com/python/cpython/refs/tags/v3.13.5/Lib/test/test_fake.py
|
|
# ======= END DYNAMO PATCH =======
|
|
|
|
"""
|
|
_FAKE_BODY = b"""\
|
|
def foo():
|
|
return 1
|
|
|
|
# pad01
|
|
# pad02
|
|
# pad03
|
|
# pad04
|
|
# pad05
|
|
# pad06
|
|
# pad07
|
|
# pad08
|
|
|
|
def bar():
|
|
return 2
|
|
|
|
# pad09
|
|
# pad10
|
|
# pad11
|
|
# pad12
|
|
# pad13
|
|
# pad14
|
|
# pad15
|
|
# pad16
|
|
|
|
def baz():
|
|
return 3
|
|
"""
|
|
|
|
|
|
def _fake_pair_bytes():
|
|
"""Pristine / adapted with one middle hunk, plus an edit outside that hunk."""
|
|
pristine = _FAKE_BODY
|
|
adapted = _FAKE_HEADER + _FAKE_BODY.replace(b" return 2", b" return 99")
|
|
edited = adapted.replace(b"def baz():", _OUTSIDE_HUNK_LINE + b"def baz():")
|
|
diff_text = diff_sync.make_unified_diff(pristine, adapted, _FAKE_REL)
|
|
return pristine, adapted, edited, diff_text
|
|
|
|
|
|
def _bool_pair():
|
|
for diff_path in diff_sync.iter_diff_paths():
|
|
if diff_path.name == "test_bool.diff":
|
|
py_path = diff_path.with_suffix(".py")
|
|
if not py_path.is_file():
|
|
raise AssertionError(f"missing {py_path}")
|
|
return py_path, diff_path
|
|
raise AssertionError("test_bool.py pair not found")
|
|
|
|
|
|
@unittest.skipIf(shutil.which("git") is None, "git binary required")
|
|
class CPythonDiffSyncTests(TestCase):
|
|
def test_verify_all_passes_on_checked_in_tree(self):
|
|
errors = diff_sync.verify_all()
|
|
self.assertEqual(
|
|
errors,
|
|
[],
|
|
"CPython .diff sync check failed. Regenerate with:\n"
|
|
" python tools/regenerate_cpython_diffs.py\n"
|
|
+ "\n".join(f"{rel}: {msg}" for rel, msg in errors),
|
|
)
|
|
|
|
def test_parse_header_requires_source_url(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
py = Path(tmp) / "test_fake.py"
|
|
py.write_text("# no url here\n", encoding="utf-8")
|
|
with self.assertRaises(ValueError):
|
|
diff_sync.parse_header(py)
|
|
|
|
def test_adapted_blob_drift_fails_verify(self):
|
|
py_path, diff_path = _bool_pair()
|
|
text = diff_path.read_text(encoding="utf-8")
|
|
_before, after = diff_sync.parse_index(text)
|
|
# Corrupt the after-hash in a temp copy of the diff (do not touch the tree).
|
|
corrupted = re.sub(
|
|
rf"(index [0-9a-f]+\.\.){re.escape(after)}",
|
|
rf"\g<1>{'0' * len(after)}",
|
|
text,
|
|
count=1,
|
|
)
|
|
self.assertNotEqual(corrupted, text)
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
tmp_diff = Path(tmp) / "test_bool.diff"
|
|
tmp_diff.write_bytes(corrupted.encode("utf-8"))
|
|
errors = diff_sync.verify_pair(py_path, tmp_diff)
|
|
self.assertTrue(
|
|
any("stale" in msg or "adapted file hash" in msg for _, msg in errors),
|
|
errors,
|
|
)
|
|
|
|
def test_missing_index_line_fails_verify(self):
|
|
py_path, diff_path = _bool_pair()
|
|
text = (
|
|
"\n".join(
|
|
line
|
|
for line in diff_path.read_text(encoding="utf-8").split("\n")
|
|
if not line.startswith("index ")
|
|
)
|
|
+ "\n"
|
|
)
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
tmp_diff = Path(tmp) / "test_bool.diff"
|
|
tmp_diff.write_bytes(text.encode("utf-8"))
|
|
errors = diff_sync.verify_pair(py_path, tmp_diff)
|
|
self.assertTrue(
|
|
any("missing git index line" in msg for _, msg in errors), errors
|
|
)
|
|
|
|
def test_all_pairs_share_one_upstream_tag(self):
|
|
# Do not hardcode a CPython tag — the suite will move (e.g. to 3.15).
|
|
tags = {
|
|
diff_sync.parse_header(diff.with_suffix(".py"))[0]
|
|
for diff in diff_sync.iter_diff_paths()
|
|
if diff.with_suffix(".py").is_file()
|
|
}
|
|
self.assertEqual(len(tags), 1, tags)
|
|
|
|
def test_index_hashes_match_blob_objects(self):
|
|
py_path, diff_path = _bool_pair()
|
|
before, after = diff_sync.parse_index(diff_path.read_text(encoding="utf-8"))
|
|
adapted = diff_sync.normalize_bytes(py_path.read_bytes())
|
|
self.assertEqual(diff_sync.git_hash_object(adapted), after)
|
|
pristine = diff_sync.reverse_apply_to_pristine(py_path, diff_path)
|
|
self.assertEqual(diff_sync.git_hash_object(pristine), before)
|
|
|
|
def _reverse_apply_bytes(self, base: bytes, diff_text: str) -> bytes:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
py = root / _FAKE_REL
|
|
py.parent.mkdir(parents=True)
|
|
py.write_bytes(base)
|
|
diff_path = py.with_suffix(".diff")
|
|
diff_sync.write_utf8(diff_path, diff_text)
|
|
with patch.object(diff_sync, "REPO_ROOT", root):
|
|
return diff_sync.reverse_apply_to_pristine(py, diff_path)
|
|
|
|
def test_check_pristine_anchor_refuses_outside_hunk_edit(self):
|
|
_pristine, adapted, edited, diff_text = _fake_pair_bytes()
|
|
old_before, _ = diff_sync.parse_index(diff_text)
|
|
reconstructed = self._reverse_apply_bytes(edited, diff_text)
|
|
self.assertIn(_OUTSIDE_HUNK_LINE, reconstructed)
|
|
self.assertNotEqual(diff_sync.git_hash_object(reconstructed), old_before)
|
|
with self.assertRaisesRegex(
|
|
RuntimeError, "refusing to move the upstream anchor"
|
|
):
|
|
diff_sync.check_pristine_anchor(
|
|
reconstructed, diff_text, _FAKE_TAG, _FAKE_UPSTREAM
|
|
)
|
|
matching = self._reverse_apply_bytes(adapted, diff_text)
|
|
self.assertEqual(diff_sync.git_hash_object(matching), old_before)
|
|
diff_sync.check_pristine_anchor(matching, diff_text, _FAKE_TAG, _FAKE_UPSTREAM)
|
|
# First full-index write: no index line, nothing to preserve.
|
|
diff_sync.check_pristine_anchor(
|
|
reconstructed, "diff --git a/x b/x\n", _FAKE_TAG, _FAKE_UPSTREAM
|
|
)
|
|
|
|
def _run_regen_on_fake_tree(
|
|
self,
|
|
py_bytes: bytes,
|
|
diff_text: str,
|
|
*,
|
|
force: bool,
|
|
pristine_path: Path | None,
|
|
):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
cpython_dir = root / "test" / "cpython" / "v3_13"
|
|
cpython_dir.mkdir(parents=True)
|
|
py_path = cpython_dir / "test_fake.py"
|
|
diff_path = cpython_dir / "test_fake.diff"
|
|
py_path.write_bytes(py_bytes)
|
|
diff_sync.write_utf8(diff_path, diff_text)
|
|
old_diff = diff_path.read_bytes()
|
|
old_before, _ = diff_sync.parse_index(diff_text)
|
|
with patch.object(regen.diff_sync, "CPYTHON_DIR", cpython_dir):
|
|
with patch.object(regen.diff_sync, "REPO_ROOT", root):
|
|
with patch.object(regen, "REPO_ROOT", root):
|
|
rc = regen.regenerate(
|
|
"test_fake.py", force=force, pristine_path=pristine_path
|
|
)
|
|
return (
|
|
rc,
|
|
old_before,
|
|
old_diff,
|
|
diff_path.read_bytes(),
|
|
py_path.read_bytes(),
|
|
)
|
|
|
|
def test_regenerate_refuses_outside_hunk_edit_without_pristine(self):
|
|
_pristine, _adapted, edited, diff_text = _fake_pair_bytes()
|
|
for force in (False, True):
|
|
with self.subTest(force=force):
|
|
rc, old_before, old_diff, new_diff, _py = self._run_regen_on_fake_tree(
|
|
edited, diff_text, force=force, pristine_path=None
|
|
)
|
|
self.assertEqual(rc, 1)
|
|
self.assertEqual(new_diff, old_diff)
|
|
got_before, _ = diff_sync.parse_index(new_diff.decode("utf-8"))
|
|
self.assertEqual(got_before, old_before)
|
|
|
|
def test_regenerate_force_keeps_anchor_when_in_sync(self):
|
|
_pristine, adapted, _edited, diff_text = _fake_pair_bytes()
|
|
rc, old_before, _old_diff, new_diff, _py = self._run_regen_on_fake_tree(
|
|
adapted, diff_text, force=True, pristine_path=None
|
|
)
|
|
self.assertEqual(rc, 0)
|
|
new_before, new_after = diff_sync.parse_index(new_diff.decode("utf-8"))
|
|
self.assertEqual(new_before, old_before)
|
|
self.assertEqual(diff_sync.git_hash_object(adapted), new_after)
|
|
|
|
def test_regenerate_with_pristine_records_outside_hunk_edit(self):
|
|
with tempfile.TemporaryDirectory() as ptmp:
|
|
pristine_file = Path(ptmp) / "pristine.py"
|
|
pristine, _adapted, edited, diff_text = _fake_pair_bytes()
|
|
pristine_file.write_bytes(pristine)
|
|
rc, old_before, old_diff, new_diff, written = self._run_regen_on_fake_tree(
|
|
edited, diff_text, force=False, pristine_path=pristine_file
|
|
)
|
|
self.assertEqual(rc, 0)
|
|
self.assertEqual(written, edited)
|
|
self.assertNotEqual(new_diff, old_diff)
|
|
new_before, new_after = diff_sync.parse_index(new_diff.decode("utf-8"))
|
|
self.assertEqual(new_before, old_before)
|
|
self.assertEqual(diff_sync.git_hash_object(edited), new_after)
|
|
self.assertIn(_OUTSIDE_HUNK_LINE.decode("ascii"), new_diff.decode("utf-8"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run_tests()
|