Preserve decorator rationale during canonical test copying

Reject differing decorator source blocks even when their ASTs match. Cover inline and multiline marker comments, guarded async methods, and CLI no-write behavior.

Assisted-by: OpenAI Codex:model identifier unavailable
This commit is contained in:
dot committed 2026-10-12 04:02:02 +09:00
1 parent 28623c11ef
commit c6ddfae40d
2 files changed
+94

No files matched your search

+9
View File
@@ -74,6 +74,15 @@ def copy_test(source: str, destination: str, qualified_name: str) -> str:
raise ValueError("Source and destination indentation differs")
source_lines = source.splitlines(keepends=True)
destination_lines = destination.splitlines(keepends=True)
# AST equality omits comments, including project marker rationales. Require
# the complete decorator block to match before replacing the definition.
if (
source_lines[src_start : src.lineno - 1]
!= destination_lines[dst_start : dst.lineno - 1]
):
raise ValueError(
"Source and destination decorator text differs; preserve markers explicitly"
)
definition = source_lines[src_start:src_end]
result = "".join(
destination_lines[:dst_start] + definition + destination_lines[dst_end:]
@@ -82,6 +82,91 @@ class Finder:
with self.assertRaisesRegex(ValueError, "Expected one class"):
copy_test(local, self.destination, "Finder.example")
def test_decorator_comments_must_match_exactly(self):
for source_marker, destination_marker in (
(
"@unittest.expectedFailure",
"@unittest.expectedFailure # TODO: RUSTPYTHON; reason",
),
(
"@unittest.expectedFailure # original reason",
"@unittest.expectedFailure # changed reason",
),
(
"@unittest.expectedFailure\n @other",
"@unittest.expectedFailure\n # marker rationale\n @other",
),
(
"@unittest.expectedFailureIf(\n condition,\n 'reason',\n )",
"@unittest.expectedFailureIf(\n condition, # project limitation\n 'reason',\n )",
),
):
with self.subTest(destination_marker=destination_marker):
source = self.source.replace(
" def example():",
f" {source_marker}\n def example():",
)
destination = self.destination.replace(
" def example():",
f" {destination_marker}\n def example():",
)
with self.assertRaisesRegex(ValueError, "decorator text differs"):
copy_test(source, destination, "Finder.example")
def test_identical_decorator_comments_are_preserved(self):
prefix = (
" @unittest.expectedFailure # TODO: RUSTPYTHON; reason\n"
" # Keep this rationale with the marker.\n"
" def example():"
)
for declaration in ("def example():", "async def example():"):
with self.subTest(declaration=declaration):
source = self.source.replace(" def example():", prefix)
destination = self.destination.replace(" def example():", prefix)
source = source.replace("def example():", declaration)
destination = destination.replace("def example():", declaration)
self.assertEqual(
copy_test(source, destination, "Finder.example"),
destination.replace("below 800", "below 850"),
)
def test_cli_does_not_write_on_decorator_comment_mismatch(self):
with tempfile.TemporaryDirectory() as temp:
source = Path(temp) / "source.py"
destination = Path(temp) / "destination.py"
source.write_text(
self.source.replace(
" def example():",
" @unittest.expectedFailure\n def example():",
)
)
destination.write_text(
self.destination.replace(
" def example():",
" @unittest.expectedFailure # TODO: RUSTPYTHON; reason\n"
" def example():",
)
)
before = destination.read_bytes()
with (
contextlib.redirect_stderr(io.StringIO()) as errors,
self.assertRaises(SystemExit),
):
main(
[
str(source),
str(destination),
"--test",
"Finder.example",
"--source-sha256",
hashlib.sha256(source.read_bytes()).hexdigest(),
"--destination-sha256",
hashlib.sha256(before).hexdigest(),
]
)
self.assertIn("decorator text differs", errors.getvalue())
self.assertEqual(destination.read_bytes(), before)
def test_return_annotation_and_type_parameters_are_rejected(self):
for declaration in ("def example() -> int:", "def example[T]():"):
with self.subTest(declaration=declaration):