scripts/tests/proof_generator/test_infer_moves.py
scripts/tests/proof_generator/test_infer_moves.pyBrowse 41 files
4,317 tokens
17,236 bytes
Token encoding: o200k_base
Snapshot a9fb1c3
← Back to SKILL.md
1import subprocess2import sys3from pathlib import Path4 5import pytest6 7sys.path.insert(0, str(Path(__file__).resolve().parents[2]))8 9from generator_testlib import ( # noqa: F40110 _commit,11 _free_function_move_with_module_level_caller,12 _git,13 _method_onto_class,14 _write,15)16from mechanical_refactor_proof_generator import (17 infer_recipe,18 recipe_to_script,19)20 21 22def test_infer_recipe_method_onto_class(repo: Path) -> None:23 """A method move onto a class infers the move, the call-site lowering, and the orphaned24 local import removal."""25 _method_onto_class(repo)26 recipe = infer_recipe("HEAD", str(repo))27 assert recipe.supported28 assert [29 (m["name"], m["src"], m["dst"], m["into_class"], m["dedent"])30 for m in recipe.moves31 ] == [("foo", "model.py", "comp.py", "C", 0)]32 assert recipe.lowerings == [33 {"name": "foo", "owner": "M", "path": "caller.py", "kind": "lower"}34 ]35 assert recipe.import_removals == [36 {"path": "caller.py", "text": "from model import M", "in_function": "run"}37 ]38 assert recipe.import_additions == []39 40 41def test_infer_recipe_move_before_typechecking_uses_after_anchor(repo: Path) -> None:42 """A module-level def relocated to land just above an ``if TYPE_CHECKING:`` guard cannot43 be anchored with before= (the next def sits past the guard), so the recipe anchors it with44 after=<the preceding assignment>."""45 _write(46 repo,47 **{48 "model.py": (49 "def keep():\n return 0\n\n\ndef helper(x):\n return x + 1\n"50 ),51 "util.py": (52 "from u import is_hip\n"53 "\n"54 "_is_hip = is_hip()\n"55 "\n"56 "if TYPE_CHECKING:\n"57 " from m import Thing\n"58 ),59 },60 )61 _commit(repo, "base")62 _write(63 repo,64 **{65 "model.py": "def keep():\n return 0\n",66 "util.py": (67 "from u import is_hip\n"68 "\n"69 "_is_hip = is_hip()\n"70 "\n"71 "\n"72 "def helper(x):\n"73 " return x + 1\n"74 "\n"75 "\n"76 "if TYPE_CHECKING:\n"77 " from m import Thing\n"78 ),79 },80 )81 _commit(repo, "move helper above the TYPE_CHECKING guard")82 recipe = infer_recipe("HEAD", str(repo))83 assert recipe.supported84 assert len(recipe.moves) == 185 move = recipe.moves[0]86 assert move["name"] == "helper" and move["dst"] == "util.py"87 assert move["before"] is None88 assert move["after"] == "_is_hip"89 90 91def test_infer_recipe_free_function_move_uses_requalify(repo: Path) -> None:92 """A move to a module-level free function dedents and requalifies the call site93 (drops the qualifier), rather than lowering a receiver."""94 _write(95 repo,96 **{97 "model.py": (98 "class M:\n"99 " @staticmethod\n"100 " def foo(x):\n"101 " return x + 1\n"102 "\n"103 " def other(self):\n"104 " return 0\n"105 ),106 "util.py": "import os\n",107 "caller.py": (108 "class K:\n"109 " def run(self):\n"110 " from model import M\n"111 "\n"112 " return M.foo(9)\n"113 ),114 },115 )116 _commit(repo, "base")117 _write(118 repo,119 **{120 "model.py": "class M:\n def other(self):\n return 0\n",121 "util.py": "import os\n\n\ndef foo(x):\n return x + 1\n",122 "caller.py": ("class K:\n def run(self):\n return foo(9)\n"),123 },124 )125 _commit(repo, "move foo to util as a free function")126 recipe = infer_recipe("HEAD", str(repo))127 assert [(m["name"], m["into_class"], m["dedent"]) for m in recipe.moves] == [128 ("foo", None, 4)129 ]130 assert recipe.lowerings == [131 {"name": "foo", "owner": "M", "path": "caller.py", "kind": "requalify"}132 ]133 134 135def test_infer_recipe_excludes_the_moved_bodys_own_call(repo: Path) -> None:136 """A same-named call on a different receiver inside the moved body is not a caller137 lowering (only `M.foo(...)` is, not `worker.foo(...)`)."""138 _write(139 repo,140 **{141 "model.py": (142 "class M:\n"143 " @staticmethod\n"144 " def foo(self, x):\n"145 " worker.foo(x)\n"146 " return x\n"147 "\n"148 " def other(self):\n"149 " return 0\n"150 ),151 "comp.py": "class C:\n def keep(self):\n return 1\n",152 },153 )154 _commit(repo, "base")155 _write(156 repo,157 **{158 "model.py": "class M:\n def other(self):\n return 0\n",159 "comp.py": (160 "class C:\n"161 " def keep(self):\n"162 " return 1\n"163 "\n"164 " def foo(self, x):\n"165 " worker.foo(x)\n"166 " return x\n"167 ),168 },169 )170 _commit(repo, "move foo onto C")171 recipe = infer_recipe("HEAD", str(repo))172 assert recipe.lowerings == []173 174 175def test_infer_recipe_skips_nested_functions(repo: Path) -> None:176 """A def nested inside a moved method is not inferred as its own move."""177 _write(178 repo,179 **{180 "model.py": (181 "class M:\n"182 " def wrap(self):\n"183 " def inner(z):\n"184 " return z\n"185 " return inner\n"186 "\n"187 " def other(self):\n"188 " return 0\n"189 ),190 "comp.py": "class C:\n def keep(self):\n return 1\n",191 },192 )193 _commit(repo, "base")194 _write(195 repo,196 **{197 "model.py": "class M:\n def other(self):\n return 0\n",198 "comp.py": (199 "class C:\n"200 " def keep(self):\n"201 " return 1\n"202 "\n"203 " def wrap(self):\n"204 " def inner(z):\n"205 " return z\n"206 " return inner\n"207 ),208 },209 )210 _commit(repo, "move wrap onto C")211 recipe = infer_recipe("HEAD", str(repo))212 names = [m["name"] for m in recipe.moves]213 assert names == ["wrap"]214 assert any("inner" in n for n in recipe.notes)215 216 217def test_infer_recipe_free_function_source_move_repaths_caller(repo: Path) -> None:218 """A free function moved to an existing module becomes a move_symbol with the call left219 bare; a caller's function-scoped import is repathed."""220 _write(221 repo,222 **{223 "model.py": "def keep():\n return 0\n\n\ndef resolve(m):\n return m\n",224 "util.py": "import os\n",225 "caller.py": (226 "class K:\n"227 " def run(self):\n"228 " from model import resolve\n"229 "\n"230 " return resolve(self.m)\n"231 ),232 },233 )234 _commit(repo, "base")235 _write(236 repo,237 **{238 "model.py": "def keep():\n return 0\n",239 "util.py": "import os\n\n\ndef resolve(m):\n return m\n",240 "caller.py": (241 "class K:\n"242 " def run(self):\n"243 " from util import resolve\n"244 "\n"245 " return resolve(self.m)\n"246 ),247 },248 )249 _commit(repo, "move resolve to util")250 recipe = infer_recipe("HEAD", str(repo))251 assert recipe.supported252 assert [(m["name"], m["src"], m["dst"], m["into_class"]) for m in recipe.moves] == [253 ("resolve", "model.py", "util.py", None)254 ]255 assert recipe.lowerings == []256 assert recipe.repaths == [257 {258 "path": "caller.py",259 "old_module": "model",260 "new_module": "util",261 "name": "resolve",262 }263 ]264 265 266def test_infer_recipe_survives_a_non_python_file_in_the_commit(repo: Path) -> None:267 """A commit also touching a .md file infers the move and notes the non-Python path."""268 _write(269 repo,270 **{271 "model.py": "def foo():\n return 1\n\n\ndef keep():\n return 0\n",272 "util.py": "x = 1\n",273 "README.md": "hello\n",274 },275 )276 _commit(repo, "base")277 _write(278 repo,279 **{280 "model.py": "def keep():\n return 0\n",281 "util.py": "x = 1\n\n\ndef foo():\n return 1\n",282 "README.md": "hello world, this is plain markdown text\n",283 },284 )285 commit = _commit(repo, "move foo and touch docs")286 287 recipe = infer_recipe(commit, str(repo))288 289 assert [mv["name"] for mv in recipe.moves] == ["foo"]290 assert any("README.md" in note for note in recipe.notes)291 292 293def test_infer_recipe_records_the_source_class_for_disambiguation(repo: Path) -> None:294 """A method move carries from_class so the cut cannot hit a same-named other method."""295 _write(296 repo,297 **{298 "model.py": (299 "class M:\n"300 " def foo(self, x):\n"301 " return x + 1\n"302 "\n"303 "\n"304 "class Other:\n"305 " def foo(self, x):\n"306 " return x + 2\n"307 ),308 "comp.py": "class C:\n def keep(self):\n return 1\n",309 },310 )311 _commit(repo, "base")312 _write(313 repo,314 **{315 "model.py": (316 "class M:\n"317 " pass\n"318 "\n"319 "\n"320 "class Other:\n"321 " def foo(self, x):\n"322 " return x + 2\n"323 ),324 "comp.py": (325 "class C:\n"326 " def keep(self):\n"327 " return 1\n"328 "\n"329 " def foo(self, x):\n"330 " return x + 1\n"331 ),332 },333 )334 commit = _commit(repo, "move M.foo onto C")335 336 recipe = infer_recipe(commit, str(repo))337 338 assert [mv["from_class"] for mv in recipe.moves] == ["M"]339 script = recipe_to_script(recipe, "move M.foo onto C")340 assert "from_class='M'" in script341 342 343def test_infer_recipe_module_level_def_shadowed_by_method_name(repo: Path) -> None:344 """A column-0 cut resolves to the module-level def even when a method shares its name."""345 _write(346 repo,347 **{348 "model.py": (349 "def foo(*, x):\n"350 " return x + 1\n"351 "\n"352 "\n"353 "class M:\n"354 " def foo(self):\n"355 " return foo(x=self.x)\n"356 ),357 "util.py": "def keep():\n return 1\n",358 },359 )360 _commit(repo, "base")361 _write(362 repo,363 **{364 "model.py": (365 "from util import foo\n"366 "\n"367 "\n"368 "class M:\n"369 " def foo(self):\n"370 " return foo(x=self.x)\n"371 ),372 "util.py": (373 "def keep():\n return 1\n\n\ndef foo(*, x):\n return x + 1\n"374 ),375 },376 )377 commit = _commit(repo, "move module-level foo to util")378 379 recipe = infer_recipe(commit, str(repo))380 381 assert recipe.supported382 assert [mv["name"] for mv in recipe.moves] == ["foo"]383 assert recipe.moves[0]["from_class"] is None384 assert recipe.moves[0]["into_class"] is None385 386 387def test_infer_recipe_class_move_between_existing_files(repo: Path) -> None:388 """A top-level class relocated to an existing module moves whole; its methods do not."""389 _write(390 repo,391 **{392 "model.py": (393 "class Payload:\n"394 " def get(self):\n"395 " return 1\n"396 "\n"397 "\n"398 "def stay():\n"399 " return 2\n"400 ),401 "comp.py": "def keep():\n return 3\n",402 },403 )404 _commit(repo, "base")405 _write(406 repo,407 **{408 "model.py": "def stay():\n return 2\n",409 "comp.py": (410 "def keep():\n"411 " return 3\n"412 "\n"413 "\n"414 "class Payload:\n"415 " def get(self):\n"416 " return 1\n"417 ),418 },419 )420 commit = _commit(repo, "move Payload to comp")421 422 recipe = infer_recipe(commit, str(repo))423 424 assert recipe.supported425 assert [mv["name"] for mv in recipe.moves] == ["Payload"]426 assert recipe.moves[0]["from_class"] is None427 assert recipe.moves[0]["into_class"] is None428 429 430def test_infer_recipe_move_leaving_a_forwarding_delegate(repo: Path) -> None:431 """A same-named stub re-added to the source infers leave_delegate on the move."""432 _write(433 repo,434 **{435 "model.py": ("class M:\n def work(self, x):\n return x + 1\n"),436 "comp.py": "class C:\n def keep(self):\n return 1\n",437 },438 )439 _commit(repo, "base")440 _write(441 repo,442 **{443 "model.py": (444 "class M:\n def work(self, x):\n return self.comp.work(x)\n"445 ),446 "comp.py": (447 "class C:\n"448 " def keep(self):\n"449 " return 1\n"450 "\n"451 " def work(self, x):\n"452 " return x + 1\n"453 ),454 },455 )456 commit = _commit(repo, "move M.work onto C, leaving a delegate")457 458 recipe = infer_recipe(commit, str(repo))459 460 assert recipe.supported461 assert [mv["name"] for mv in recipe.moves] == ["work"]462 assert recipe.moves[0]["dst"] == "comp.py"463 assert recipe.moves[0]["leave_delegate"] == "comp"464 assert recipe.moves[0]["delegate_name"] is None465 script = recipe_to_script(recipe, "move with delegate")466 assert "leave_delegate='comp'" in script467 468 469def test_infer_recipe_constant_relocated_with_the_move(repo: Path) -> None:470 """A module constant that vanished from the source and appeared in the existing471 destination becomes a move_assign."""472 _write(473 repo,474 **{475 "model.py": (476 "RATIO = 3\n"477 "\n"478 "\n"479 "def work(x):\n"480 " return x * RATIO\n"481 "\n"482 "\n"483 "def stay():\n"484 " return 1\n"485 ),486 "comp.py": "import os\n\n\ndef keep():\n return 2\n",487 },488 )489 _commit(repo, "base")490 _write(491 repo,492 **{493 "model.py": "def stay():\n return 1\n",494 "comp.py": (495 "import os\n"496 "\n"497 "RATIO = 3\n"498 "\n"499 "\n"500 "def keep():\n"501 " return 2\n"502 "\n"503 "\n"504 "def work(x):\n"505 " return x * RATIO\n"506 ),507 },508 )509 commit = _commit(repo, "move work + RATIO to comp")510 511 recipe = infer_recipe(commit, str(repo))512 513 assert recipe.supported514 assert [am["name"] for am in recipe.assign_moves] == ["RATIO"]515 script = recipe_to_script(recipe, "move with constant")516 assert "move_assign" in script517 518 519def test_infer_recipe_in_file_method_reorder(repo: Path) -> None:520 """A method cut and re-inserted elsewhere in the same class (no other file gains it) infers521 an in-file move_symbol (src == dst) anchored above its new next sibling."""522 _write(523 repo,524 **{525 "m.py": (526 "class C:\n"527 " def a(self):\n"528 " return 1\n"529 "\n"530 " def b(self):\n"531 " return 2\n"532 "\n"533 " def c(self):\n"534 " return 3\n"535 )536 },537 )538 _commit(repo, "base")539 _write(540 repo,541 **{542 "m.py": (543 "class C:\n"544 " def c(self):\n"545 " return 3\n"546 "\n"547 " def a(self):\n"548 " return 1\n"549 "\n"550 " def b(self):\n"551 " return 2\n"552 )553 },554 )555 commit = _commit(repo, "move c above a")556 recipe = infer_recipe(commit, str(repo))557 assert recipe.supported558 assert len(recipe.moves) == 1559 mv = recipe.moves[0]560 assert mv["name"] == "c" and mv["src"] == "m.py" and mv["dst"] == "m.py"561 assert mv["into_class"] == "C" and mv["before"] == "a"562