Enhance mechanical-refactor-verify skill with a whole-chain verifier, new relocation primitives, and generator inference (#30585)
This commit is contained in:
+322
@@ -0,0 +1,322 @@
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
|
||||
from generator_testlib import _commit, _git, _write # noqa: F401
|
||||
from mechanical_refactor_proof_generator import (
|
||||
build_repro,
|
||||
infer_recipe,
|
||||
recipe_to_script,
|
||||
)
|
||||
|
||||
|
||||
def test_infer_extract_function_with_returned_local(repo: Path) -> None:
|
||||
"""A block ending in ``pool = make(...)`` carved into a helper that returns ``pool`` infers
|
||||
an extract_function whose body is the verbatim block and whose return_text is authored.
|
||||
"""
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"kv.py": (
|
||||
"class C:\n"
|
||||
" def dispatch(self, n):\n"
|
||||
" base = self.setup()\n"
|
||||
" if self.flag:\n"
|
||||
" x = self.a\n"
|
||||
" y = x + n\n"
|
||||
" pool = make(\n"
|
||||
" a=x,\n"
|
||||
" b=y,\n"
|
||||
" )\n"
|
||||
" return pool\n"
|
||||
"\n"
|
||||
" def keep(self):\n"
|
||||
" return 0\n"
|
||||
)
|
||||
},
|
||||
)
|
||||
_commit(repo, "base")
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"kv.py": (
|
||||
"class C:\n"
|
||||
" def dispatch(self, n):\n"
|
||||
" base = self.setup()\n"
|
||||
" if self.flag:\n"
|
||||
" pool = self._build_pool(n=n)\n"
|
||||
" return pool\n"
|
||||
"\n"
|
||||
" def _build_pool(self, *, n):\n"
|
||||
" x = self.a\n"
|
||||
" y = x + n\n"
|
||||
" pool = make(\n"
|
||||
" a=x,\n"
|
||||
" b=y,\n"
|
||||
" )\n"
|
||||
" return pool\n"
|
||||
"\n"
|
||||
" def keep(self):\n"
|
||||
" return 0\n"
|
||||
)
|
||||
},
|
||||
)
|
||||
_commit(repo, "extract _build_pool from dispatch")
|
||||
recipe = infer_recipe("HEAD", str(repo))
|
||||
assert recipe.supported
|
||||
assert recipe.moves == []
|
||||
assert len(recipe.extract_functions) == 1
|
||||
ex = recipe.extract_functions[0]
|
||||
assert ex["name"] == "_build_pool"
|
||||
assert ex["src"] == "kv.py" and ex["dst"] == "kv.py"
|
||||
assert ex["into_class"] == "C"
|
||||
assert ex["before"] == "keep"
|
||||
assert ex["body_indent"] == 12
|
||||
assert ex["body"] == (
|
||||
" x = self.a\n"
|
||||
" y = x + n\n"
|
||||
" pool = make(\n"
|
||||
" a=x,\n"
|
||||
" b=y,\n"
|
||||
" )\n"
|
||||
)
|
||||
assert ex["call"] == " pool = self._build_pool(n=n)\n"
|
||||
assert ex["return_text"] == " return pool"
|
||||
assert ex["signature"] == " def _build_pool(self, *, n):\n"
|
||||
|
||||
|
||||
def test_infer_extract_function_keeps_leading_comment_in_body(repo: Path) -> None:
|
||||
"""A block whose first line is a comment extracts with that comment in the body, not
|
||||
absorbed into the authored signature (which is the def header through its colon only).
|
||||
"""
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"kv.py": (
|
||||
"class C:\n"
|
||||
" def dispatch(self, n):\n"
|
||||
" if self.flag:\n"
|
||||
" # pick the pool class for this backend\n"
|
||||
" cls = PoolA\n"
|
||||
" pool = cls(n)\n"
|
||||
" return pool\n"
|
||||
)
|
||||
},
|
||||
)
|
||||
_commit(repo, "base")
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"kv.py": (
|
||||
"class C:\n"
|
||||
" def dispatch(self, n):\n"
|
||||
" if self.flag:\n"
|
||||
" pool = self._build_pool(n=n)\n"
|
||||
" return pool\n"
|
||||
"\n"
|
||||
" def _build_pool(self, *, n):\n"
|
||||
" # pick the pool class for this backend\n"
|
||||
" cls = PoolA\n"
|
||||
" pool = cls(n)\n"
|
||||
" return pool\n"
|
||||
)
|
||||
},
|
||||
)
|
||||
commit = _commit(repo, "extract _build_pool with a leading comment")
|
||||
recipe = infer_recipe(commit, str(repo))
|
||||
assert len(recipe.extract_functions) == 1
|
||||
ex = recipe.extract_functions[0]
|
||||
assert ex["signature"] == " def _build_pool(self, *, n):\n"
|
||||
assert ex["body"].lstrip().startswith("# pick the pool class")
|
||||
assert build_repro(recipe, repo_root=str(repo)).run() == ""
|
||||
|
||||
|
||||
def test_infer_extract_function_no_return_text_when_body_is_whole_helper(
|
||||
repo: Path,
|
||||
) -> None:
|
||||
"""When the helper body reproduces the source block with no trailing return, return_text
|
||||
is None (the block is a side-effecting statement sequence, not a value producer)."""
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"kv.py": (
|
||||
"class C:\n"
|
||||
" def run(self):\n"
|
||||
" self.pre()\n"
|
||||
" self.log(1)\n"
|
||||
" self.log(2)\n"
|
||||
" self.post()\n"
|
||||
)
|
||||
},
|
||||
)
|
||||
_commit(repo, "base")
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"kv.py": (
|
||||
"class C:\n"
|
||||
" def run(self):\n"
|
||||
" self.pre()\n"
|
||||
" self._emit()\n"
|
||||
" self.post()\n"
|
||||
"\n"
|
||||
" def _emit(self):\n"
|
||||
" self.log(1)\n"
|
||||
" self.log(2)\n"
|
||||
)
|
||||
},
|
||||
)
|
||||
_commit(repo, "extract _emit from run")
|
||||
recipe = infer_recipe("HEAD", str(repo))
|
||||
assert recipe.supported
|
||||
assert len(recipe.extract_functions) == 1
|
||||
ex = recipe.extract_functions[0]
|
||||
assert ex["name"] == "_emit"
|
||||
assert ex["return_text"] is None
|
||||
assert ex["call"] == " self._emit()\n"
|
||||
|
||||
|
||||
def test_infer_extract_function_edited_body_does_not_pass(repo: Path) -> None:
|
||||
"""A helper whose body was edited (not a verbatim cut) never yields a false pass: the
|
||||
reproduction's byte-diff surfaces the bundled change as a non-empty residual."""
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"kv.py": (
|
||||
"class C:\n"
|
||||
" def run(self):\n"
|
||||
" self.pre()\n"
|
||||
" self.log(1)\n"
|
||||
" self.post()\n"
|
||||
)
|
||||
},
|
||||
)
|
||||
_commit(repo, "base")
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"kv.py": (
|
||||
"class C:\n"
|
||||
" def run(self):\n"
|
||||
" self.pre()\n"
|
||||
" self._emit()\n"
|
||||
" self.post()\n"
|
||||
"\n"
|
||||
" def _emit(self):\n"
|
||||
" self.log(2)\n"
|
||||
)
|
||||
},
|
||||
)
|
||||
commit = _commit(repo, "extract _emit but change the arg")
|
||||
recipe = infer_recipe(commit, str(repo))
|
||||
residual = build_repro(recipe, repo_root=str(repo)).run()
|
||||
assert residual != ""
|
||||
|
||||
|
||||
def test_infer_extract_function_when_block_and_call_share_closing_paren(
|
||||
repo: Path,
|
||||
) -> None:
|
||||
"""The removed block and its replacement call both end in a lone ``)``; the prefix/suffix
|
||||
split must not absorb that shared line, or the extracted body loses its final line.
|
||||
"""
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"kv.py": (
|
||||
"class C:\n"
|
||||
" def dispatch(self, n):\n"
|
||||
" if self.flag:\n"
|
||||
" pool = make_pool(\n"
|
||||
" a=n,\n"
|
||||
" b=self.b,\n"
|
||||
" )\n"
|
||||
" return pool\n"
|
||||
)
|
||||
},
|
||||
)
|
||||
_commit(repo, "base")
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"kv.py": (
|
||||
"class C:\n"
|
||||
" def dispatch(self, n):\n"
|
||||
" if self.flag:\n"
|
||||
" pool = self._build_pool(\n"
|
||||
" n=n,\n"
|
||||
" )\n"
|
||||
" return pool\n"
|
||||
"\n"
|
||||
" def _build_pool(self, *, n):\n"
|
||||
" pool = make_pool(\n"
|
||||
" a=n,\n"
|
||||
" b=self.b,\n"
|
||||
" )\n"
|
||||
" return pool\n"
|
||||
)
|
||||
},
|
||||
)
|
||||
commit = _commit(repo, "extract _build_pool")
|
||||
recipe = infer_recipe(commit, str(repo))
|
||||
assert len(recipe.extract_functions) == 1
|
||||
ex = recipe.extract_functions[0]
|
||||
assert ex["body"].rstrip().endswith(")")
|
||||
assert ex["return_text"] == " return pool"
|
||||
assert build_repro(recipe, repo_root=str(repo)).run() == ""
|
||||
|
||||
|
||||
def test_emitted_script_passes_on_extract_function(repo: Path, tmp_path: Path) -> None:
|
||||
"""The recipe for an extract_function reproduces the commit byte-for-byte (bare repo, no
|
||||
formatter) so build_repro returns an empty residual."""
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"kv.py": (
|
||||
"class C:\n"
|
||||
" def dispatch(self, n):\n"
|
||||
" base = self.setup()\n"
|
||||
" if self.flag:\n"
|
||||
" x = self.a\n"
|
||||
" pool = make(\n"
|
||||
" a=x,\n"
|
||||
" )\n"
|
||||
" return pool\n"
|
||||
"\n"
|
||||
" def keep(self):\n"
|
||||
" return 0\n"
|
||||
)
|
||||
},
|
||||
)
|
||||
_commit(repo, "base")
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"kv.py": (
|
||||
"class C:\n"
|
||||
" def dispatch(self, n):\n"
|
||||
" base = self.setup()\n"
|
||||
" if self.flag:\n"
|
||||
" pool = self._build_pool(n=n)\n"
|
||||
" return pool\n"
|
||||
"\n"
|
||||
" def _build_pool(self, *, n):\n"
|
||||
" x = self.a\n"
|
||||
" pool = make(\n"
|
||||
" a=x,\n"
|
||||
" )\n"
|
||||
" return pool\n"
|
||||
"\n"
|
||||
" def keep(self):\n"
|
||||
" return 0\n"
|
||||
)
|
||||
},
|
||||
)
|
||||
commit = _commit(repo, "extract _build_pool from dispatch")
|
||||
recipe = infer_recipe(commit, str(repo))
|
||||
residual = build_repro(recipe, repo_root=str(repo)).run()
|
||||
assert residual == "", residual
|
||||
assert "extract_function" in recipe_to_script(recipe, "extract")
|
||||
+280
@@ -38,6 +38,56 @@ def test_infer_recipe_method_onto_class(repo: Path) -> None:
|
||||
assert recipe.import_additions == []
|
||||
|
||||
|
||||
def test_infer_recipe_move_before_typechecking_uses_after_anchor(repo: Path) -> None:
|
||||
"""A module-level def relocated to land just above an ``if TYPE_CHECKING:`` guard cannot
|
||||
be anchored with before= (the next def sits past the guard), so the recipe anchors it with
|
||||
after=<the preceding assignment>."""
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"model.py": (
|
||||
"def keep():\n return 0\n\n\ndef helper(x):\n return x + 1\n"
|
||||
),
|
||||
"util.py": (
|
||||
"from u import is_hip\n"
|
||||
"\n"
|
||||
"_is_hip = is_hip()\n"
|
||||
"\n"
|
||||
"if TYPE_CHECKING:\n"
|
||||
" from m import Thing\n"
|
||||
),
|
||||
},
|
||||
)
|
||||
_commit(repo, "base")
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"model.py": "def keep():\n return 0\n",
|
||||
"util.py": (
|
||||
"from u import is_hip\n"
|
||||
"\n"
|
||||
"_is_hip = is_hip()\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def helper(x):\n"
|
||||
" return x + 1\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"if TYPE_CHECKING:\n"
|
||||
" from m import Thing\n"
|
||||
),
|
||||
},
|
||||
)
|
||||
_commit(repo, "move helper above the TYPE_CHECKING guard")
|
||||
recipe = infer_recipe("HEAD", str(repo))
|
||||
assert recipe.supported
|
||||
assert len(recipe.moves) == 1
|
||||
move = recipe.moves[0]
|
||||
assert move["name"] == "helper" and move["dst"] == "util.py"
|
||||
assert move["before"] is None
|
||||
assert move["after"] == "_is_hip"
|
||||
|
||||
|
||||
def test_infer_recipe_free_function_move_uses_requalify(repo: Path) -> None:
|
||||
"""A move to a module-level free function dedents and requalifies the call site
|
||||
(drops the qualifier), rather than lowering a receiver."""
|
||||
@@ -288,3 +338,233 @@ def test_infer_recipe_records_the_source_class_for_disambiguation(repo: Path) ->
|
||||
assert [mv["from_class"] for mv in recipe.moves] == ["M"]
|
||||
script = recipe_to_script(recipe, "move M.foo onto C")
|
||||
assert "from_class='M'" in script
|
||||
|
||||
|
||||
def test_infer_recipe_module_level_def_shadowed_by_method_name(repo: Path) -> None:
|
||||
"""A column-0 cut resolves to the module-level def even when a method shares its name."""
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"model.py": (
|
||||
"def foo(*, x):\n"
|
||||
" return x + 1\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"class M:\n"
|
||||
" def foo(self):\n"
|
||||
" return foo(x=self.x)\n"
|
||||
),
|
||||
"util.py": "def keep():\n return 1\n",
|
||||
},
|
||||
)
|
||||
_commit(repo, "base")
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"model.py": (
|
||||
"from util import foo\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"class M:\n"
|
||||
" def foo(self):\n"
|
||||
" return foo(x=self.x)\n"
|
||||
),
|
||||
"util.py": (
|
||||
"def keep():\n"
|
||||
" return 1\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def foo(*, x):\n"
|
||||
" return x + 1\n"
|
||||
),
|
||||
},
|
||||
)
|
||||
commit = _commit(repo, "move module-level foo to util")
|
||||
|
||||
recipe = infer_recipe(commit, str(repo))
|
||||
|
||||
assert recipe.supported
|
||||
assert [mv["name"] for mv in recipe.moves] == ["foo"]
|
||||
assert recipe.moves[0]["from_class"] is None
|
||||
assert recipe.moves[0]["into_class"] is None
|
||||
|
||||
|
||||
def test_infer_recipe_class_move_between_existing_files(repo: Path) -> None:
|
||||
"""A top-level class relocated to an existing module moves whole; its methods do not."""
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"model.py": (
|
||||
"class Payload:\n"
|
||||
" def get(self):\n"
|
||||
" return 1\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def stay():\n"
|
||||
" return 2\n"
|
||||
),
|
||||
"comp.py": "def keep():\n return 3\n",
|
||||
},
|
||||
)
|
||||
_commit(repo, "base")
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"model.py": "def stay():\n return 2\n",
|
||||
"comp.py": (
|
||||
"def keep():\n"
|
||||
" return 3\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"class Payload:\n"
|
||||
" def get(self):\n"
|
||||
" return 1\n"
|
||||
),
|
||||
},
|
||||
)
|
||||
commit = _commit(repo, "move Payload to comp")
|
||||
|
||||
recipe = infer_recipe(commit, str(repo))
|
||||
|
||||
assert recipe.supported
|
||||
assert [mv["name"] for mv in recipe.moves] == ["Payload"]
|
||||
assert recipe.moves[0]["from_class"] is None
|
||||
assert recipe.moves[0]["into_class"] is None
|
||||
|
||||
|
||||
def test_infer_recipe_move_leaving_a_forwarding_delegate(repo: Path) -> None:
|
||||
"""A same-named stub re-added to the source infers leave_delegate on the move."""
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"model.py": (
|
||||
"class M:\n" " def work(self, x):\n" " return x + 1\n"
|
||||
),
|
||||
"comp.py": "class C:\n def keep(self):\n return 1\n",
|
||||
},
|
||||
)
|
||||
_commit(repo, "base")
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"model.py": (
|
||||
"class M:\n"
|
||||
" def work(self, x):\n"
|
||||
" return self.comp.work(x)\n"
|
||||
),
|
||||
"comp.py": (
|
||||
"class C:\n"
|
||||
" def keep(self):\n"
|
||||
" return 1\n"
|
||||
"\n"
|
||||
" def work(self, x):\n"
|
||||
" return x + 1\n"
|
||||
),
|
||||
},
|
||||
)
|
||||
commit = _commit(repo, "move M.work onto C, leaving a delegate")
|
||||
|
||||
recipe = infer_recipe(commit, str(repo))
|
||||
|
||||
assert recipe.supported
|
||||
assert [mv["name"] for mv in recipe.moves] == ["work"]
|
||||
assert recipe.moves[0]["dst"] == "comp.py"
|
||||
assert recipe.moves[0]["leave_delegate"] == "comp"
|
||||
assert recipe.moves[0]["delegate_name"] is None
|
||||
script = recipe_to_script(recipe, "move with delegate")
|
||||
assert "leave_delegate='comp'" in script
|
||||
|
||||
|
||||
def test_infer_recipe_constant_relocated_with_the_move(repo: Path) -> None:
|
||||
"""A module constant that vanished from the source and appeared in the existing
|
||||
destination becomes a move_assign."""
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"model.py": (
|
||||
"RATIO = 3\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def work(x):\n"
|
||||
" return x * RATIO\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def stay():\n"
|
||||
" return 1\n"
|
||||
),
|
||||
"comp.py": "import os\n\n\ndef keep():\n return 2\n",
|
||||
},
|
||||
)
|
||||
_commit(repo, "base")
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"model.py": "def stay():\n return 1\n",
|
||||
"comp.py": (
|
||||
"import os\n"
|
||||
"\n"
|
||||
"RATIO = 3\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def keep():\n"
|
||||
" return 2\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def work(x):\n"
|
||||
" return x * RATIO\n"
|
||||
),
|
||||
},
|
||||
)
|
||||
commit = _commit(repo, "move work + RATIO to comp")
|
||||
|
||||
recipe = infer_recipe(commit, str(repo))
|
||||
|
||||
assert recipe.supported
|
||||
assert [am["name"] for am in recipe.assign_moves] == ["RATIO"]
|
||||
script = recipe_to_script(recipe, "move with constant")
|
||||
assert "move_assign" in script
|
||||
|
||||
|
||||
def test_infer_recipe_in_file_method_reorder(repo: Path) -> None:
|
||||
"""A method cut and re-inserted elsewhere in the same class (no other file gains it) infers
|
||||
an in-file move_symbol (src == dst) anchored above its new next sibling."""
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"m.py": (
|
||||
"class C:\n"
|
||||
" def a(self):\n"
|
||||
" return 1\n"
|
||||
"\n"
|
||||
" def b(self):\n"
|
||||
" return 2\n"
|
||||
"\n"
|
||||
" def c(self):\n"
|
||||
" return 3\n"
|
||||
)
|
||||
},
|
||||
)
|
||||
_commit(repo, "base")
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"m.py": (
|
||||
"class C:\n"
|
||||
" def c(self):\n"
|
||||
" return 3\n"
|
||||
"\n"
|
||||
" def a(self):\n"
|
||||
" return 1\n"
|
||||
"\n"
|
||||
" def b(self):\n"
|
||||
" return 2\n"
|
||||
)
|
||||
},
|
||||
)
|
||||
commit = _commit(repo, "move c above a")
|
||||
recipe = infer_recipe(commit, str(repo))
|
||||
assert recipe.supported
|
||||
assert len(recipe.moves) == 1
|
||||
mv = recipe.moves[0]
|
||||
assert mv["name"] == "c" and mv["src"] == "m.py" and mv["dst"] == "m.py"
|
||||
assert mv["into_class"] == "C" and mv["before"] == "a"
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
|
||||
from generator_testlib import _commit, _write # noqa: F401
|
||||
from mechanical_refactor_proof_generator import _main
|
||||
|
||||
|
||||
def _extract_function_commit(repo: Path) -> str:
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"kv.py": (
|
||||
"class C:\n"
|
||||
" def dispatch(self, n):\n"
|
||||
" x = self.a\n"
|
||||
" y = x + n\n"
|
||||
" return y\n"
|
||||
"\n"
|
||||
" def keep(self):\n"
|
||||
" return 0\n"
|
||||
)
|
||||
},
|
||||
)
|
||||
_commit(repo, "base")
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"kv.py": (
|
||||
"class C:\n"
|
||||
" def dispatch(self, n):\n"
|
||||
" y = self._combine(n=n)\n"
|
||||
" return y\n"
|
||||
"\n"
|
||||
" def _combine(self, *, n):\n"
|
||||
" x = self.a\n"
|
||||
" y = x + n\n"
|
||||
" return y\n"
|
||||
"\n"
|
||||
" def keep(self):\n"
|
||||
" return 0\n"
|
||||
)
|
||||
},
|
||||
)
|
||||
return _commit(repo, "extract _combine from dispatch")
|
||||
|
||||
|
||||
def test_single_commit_extract_function_reproduces_instead_of_unsupported(
|
||||
repo: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""A pure intra-file extract_function commit run in single-commit mode reproduces (exit 0),
|
||||
not UNSUPPORTED -- the relocates check must count extract_functions like the range path.
|
||||
"""
|
||||
sha = _extract_function_commit(repo)
|
||||
monkeypatch.chdir(repo)
|
||||
assert _main([sha]) == 0
|
||||
|
||||
|
||||
def test_single_commit_pure_rename_is_unsupported(
|
||||
repo: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""A commit that relocates no definition (a bare rename) stays UNSUPPORTED with exit 1."""
|
||||
_write(repo, **{"m.py": "def foo():\n return 1\n"})
|
||||
_commit(repo, "base")
|
||||
_write(repo, **{"m.py": "def bar():\n return 1\n"})
|
||||
sha = _commit(repo, "rename foo to bar")
|
||||
monkeypatch.chdir(repo)
|
||||
assert _main([sha]) == 1
|
||||
+140
-1
@@ -25,7 +25,8 @@ def test_recipe_to_script_is_self_contained_and_ordered(repo: Path) -> None:
|
||||
script = recipe_to_script(infer_recipe("HEAD", str(repo)), "move foo onto C")
|
||||
assert "from mechanical_refactor_reproduction_utils import Repro" in script
|
||||
assert script.index("lower_call_sites") < script.index("move_symbol")
|
||||
assert "r.run()" in script
|
||||
assert "residual = r.run()" in script
|
||||
assert "sys.exit(1 if residual else 0)" in script
|
||||
# importing nothing else from the skill keeps the script auditable in isolation
|
||||
assert "mechanical_refactor_verify_utils" not in script
|
||||
assert "mechanical_refactor_proof_generator" not in script
|
||||
@@ -40,6 +41,144 @@ def test_recipe_to_script_orders_import_ops_after_moves(repo: Path) -> None:
|
||||
assert script.index("move_symbol") < script.index("add_import")
|
||||
|
||||
|
||||
def _emit_runnable_script(repo: Path, out: Path, commit: str, subject: str) -> Path:
|
||||
"""Write the emitted script plus its util dependency into a proof-folder layout."""
|
||||
scripts_dir = out / "repro_scripts"
|
||||
scripts_dir.mkdir(parents=True, exist_ok=True)
|
||||
utils_src = Path(__file__).resolve().parents[2] / (
|
||||
"mechanical_refactor_reproduction_utils.py"
|
||||
)
|
||||
(out / "mechanical_refactor_reproduction_utils.py").write_text(
|
||||
utils_src.read_text()
|
||||
)
|
||||
script = recipe_to_script(infer_recipe(commit, str(repo)), subject)
|
||||
script_path = scripts_dir / f"{commit[:9]}.py"
|
||||
script_path.write_text(script)
|
||||
return script_path
|
||||
|
||||
|
||||
def test_emitted_script_exits_zero_on_faithful_commit(
|
||||
repo: Path, tmp_path: Path
|
||||
) -> None:
|
||||
"""Running the emitted script on a clean move exits 0 and prints the PASS verdict."""
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"model.py": "def keep():\n return 0\n\n\ndef resolve(m):\n return m\n",
|
||||
"util.py": "import os\n",
|
||||
"caller.py": (
|
||||
"from model import resolve\n\n\ndef run(m):\n return resolve(m)\n"
|
||||
),
|
||||
},
|
||||
)
|
||||
_commit(repo, "base")
|
||||
# The after-state is the primitives' exact output (this bare repo has no formatter
|
||||
# to absorb the cut's leftover blank lines, unlike a pre-commit-clean real repo).
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"model.py": "def keep():\n return 0\n\n\n",
|
||||
"util.py": "import os\n\ndef resolve(m):\n return m\n",
|
||||
"caller.py": (
|
||||
"from util import resolve\n\n\ndef run(m):\n return resolve(m)\n"
|
||||
),
|
||||
},
|
||||
)
|
||||
commit = _commit(repo, "move resolve to util")
|
||||
script_path = _emit_runnable_script(repo, tmp_path / "out", commit, "move")
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script_path)], cwd=repo, capture_output=True, text=True
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert "PASS" in result.stdout
|
||||
|
||||
|
||||
def test_emitted_script_exits_nonzero_on_bundled_change(
|
||||
repo: Path, tmp_path: Path
|
||||
) -> None:
|
||||
"""A commit bundling a non-move change makes the emitted script exit non-zero."""
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"model.py": "def keep():\n return 0\n\n\ndef resolve(m):\n return m\n",
|
||||
"util.py": "import os\n",
|
||||
},
|
||||
)
|
||||
_commit(repo, "base")
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"model.py": "def keep():\n return 99\n",
|
||||
"util.py": "import os\n\n\ndef resolve(m):\n return m\n",
|
||||
},
|
||||
)
|
||||
commit = _commit(repo, "move resolve AND change keep")
|
||||
script_path = _emit_runnable_script(repo, tmp_path / "out", commit, "dirty move")
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script_path)], cwd=repo, capture_output=True, text=True
|
||||
)
|
||||
|
||||
assert result.returncode == 1, result.stdout + result.stderr
|
||||
assert "RESIDUAL" in result.stdout
|
||||
|
||||
|
||||
def test_emitted_script_passes_on_move_above_typechecking_guard(
|
||||
repo: Path, tmp_path: Path
|
||||
) -> None:
|
||||
"""A module-level def relocated to just above an ``if TYPE_CHECKING:`` guard reproduces
|
||||
via an inferred after= anchor and the emitted script exits 0."""
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"model.py": (
|
||||
"def keep():\n return 0\n\n\ndef helper(x):\n return x + 1\n"
|
||||
),
|
||||
"util.py": (
|
||||
"from u import is_hip\n"
|
||||
"\n"
|
||||
"_is_hip = is_hip()\n"
|
||||
"\n"
|
||||
"if TYPE_CHECKING:\n"
|
||||
" from m import Thing\n"
|
||||
),
|
||||
},
|
||||
)
|
||||
_commit(repo, "base")
|
||||
# After-state = the primitive's exact output (bare repo, no formatter to absorb blanks).
|
||||
_write(
|
||||
repo,
|
||||
**{
|
||||
"model.py": "def keep():\n return 0\n\n\n",
|
||||
"util.py": (
|
||||
"from u import is_hip\n"
|
||||
"\n"
|
||||
"_is_hip = is_hip()\n"
|
||||
"\n"
|
||||
"def helper(x):\n"
|
||||
" return x + 1\n"
|
||||
"\n"
|
||||
"if TYPE_CHECKING:\n"
|
||||
" from m import Thing\n"
|
||||
),
|
||||
},
|
||||
)
|
||||
commit = _commit(repo, "move helper above the TYPE_CHECKING guard")
|
||||
script_path = _emit_runnable_script(
|
||||
repo, tmp_path / "out", commit, "after-anchor move"
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script_path)], cwd=repo, capture_output=True, text=True
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert "PASS" in result.stdout
|
||||
assert "after='_is_hip'" in script_path.read_text()
|
||||
|
||||
|
||||
def test_per_file_diff_keeps_content_lines_starting_with_plus_signs(repo: Path) -> None:
|
||||
"""An added content line beginning with '++' is collected, not mistaken for a header."""
|
||||
from mechanical_refactor_proof_generator import _per_file_diff
|
||||
|
||||
Reference in New Issue
Block a user