mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
* Preserve explicit customizations during canonical library migration Add bounded runtime, call, and doctest preservation plus explicit remaining-marker metadata. Support mapped legacy test-package migrations without losing ordered guards, and exclude cache directories from both migration paths while retaining canonical assets. Validation: 53 bounded host-Python tooling tests passed. Tests inspect source and temporary files without executing migrated fixtures. Existing copy-library cache protection is unchanged. No Lib or interpreter changes. Assisted-by: OpenAI Codex:model version unavailable * Integrate migration preservation into existing updater modules Keep migration orchestration and guarded package moves in cmd_migrate, and marker/doctest metadata in patch_spec. Reuse AST anchor validation and existing test modules, removing all eleven newly introduced helper, test, and data files. Require caller-selected doctest metadata instead of embedding syntax-case policy. Reject guarded legacy doctests before package writes. Preserve the prior 53 test scenarios and add seven focused metadata regressions; all 60 bounded host tooling tests passed. No Lib, workflow, or native changes. Assisted-by: OpenAI Codex:model version unavailable
739 lines
23 KiB
Python
739 lines
23 KiB
Python
"""Tests for patch_spec.py - core patch extraction and application."""
|
|
|
|
import ast
|
|
import unittest
|
|
|
|
from update_lib.patch_spec import (
|
|
CANONICAL_LOAD_TESTS,
|
|
COMMENT,
|
|
PATCHED_LOAD_TESTS,
|
|
PatchSpec,
|
|
UtMethod,
|
|
_find_import_insert_line,
|
|
apply_doctest_metadata,
|
|
apply_patches,
|
|
extract_doctest_guards,
|
|
extract_patches,
|
|
iter_tests,
|
|
)
|
|
|
|
|
|
class TestIterTests(unittest.TestCase):
|
|
"""Tests for iter_tests function."""
|
|
|
|
def test_iter_tests_simple(self):
|
|
"""Test iterating over test methods in a class."""
|
|
code = """
|
|
class TestFoo(unittest.TestCase):
|
|
def test_one(self):
|
|
pass
|
|
|
|
def test_two(self):
|
|
pass
|
|
"""
|
|
tree = ast.parse(code)
|
|
results = list(iter_tests(tree))
|
|
self.assertEqual(len(results), 2)
|
|
self.assertEqual(results[0][0].name, "TestFoo")
|
|
self.assertEqual(results[0][1].name, "test_one")
|
|
self.assertEqual(results[1][1].name, "test_two")
|
|
|
|
def test_iter_tests_multiple_classes(self):
|
|
"""Test iterating over multiple test classes."""
|
|
code = """
|
|
class TestFoo(unittest.TestCase):
|
|
def test_foo(self):
|
|
pass
|
|
|
|
class TestBar(unittest.TestCase):
|
|
def test_bar(self):
|
|
pass
|
|
"""
|
|
tree = ast.parse(code)
|
|
results = list(iter_tests(tree))
|
|
self.assertEqual(len(results), 2)
|
|
self.assertEqual(results[0][0].name, "TestFoo")
|
|
self.assertEqual(results[1][0].name, "TestBar")
|
|
|
|
def test_iter_tests_async(self):
|
|
"""Test iterating over async test methods."""
|
|
code = """
|
|
class TestAsync(unittest.TestCase):
|
|
async def test_async(self):
|
|
pass
|
|
"""
|
|
tree = ast.parse(code)
|
|
results = list(iter_tests(tree))
|
|
self.assertEqual(len(results), 1)
|
|
self.assertEqual(results[0][1].name, "test_async")
|
|
|
|
|
|
class TestExtractPatches(unittest.TestCase):
|
|
"""Tests for extract_patches function."""
|
|
|
|
def test_class_conditional_method_markers_are_preserved(self):
|
|
code = """class Owner:
|
|
if enabled:
|
|
@unittest.expectedFailure # TODO: RUSTPYTHON
|
|
def test_value(self):
|
|
return 42
|
|
"""
|
|
patches = extract_patches(code)
|
|
self.assertIn("test_value", patches["Owner"])
|
|
clean = code.replace(
|
|
" @unittest.expectedFailure # TODO: RUSTPYTHON\n", ""
|
|
)
|
|
result = apply_patches(clean, patches)
|
|
self.assertIn(" @unittest.expectedFailure", result)
|
|
method = list(iter_tests(ast.parse(result)))[0][1]
|
|
self.assertEqual(method.col_offset, 8)
|
|
self.assertEqual(ast.dump(method.body[0]), "Return(value=Constant(value=42))")
|
|
|
|
def test_nested_class_markers_do_not_target_module_names(self):
|
|
code = """class Container:
|
|
class Target:
|
|
@unittest.expectedFailure # TODO: RUSTPYTHON
|
|
def test_value(self):
|
|
pass
|
|
def factory():
|
|
class Target:
|
|
@unittest.expectedFailure # TODO: RUSTPYTHON
|
|
def test_value(self):
|
|
pass
|
|
"""
|
|
self.assertEqual(extract_patches(code), {})
|
|
|
|
def test_bare_marked_skip_roundtrip(self):
|
|
clean = """class Owner:
|
|
def test_value(self):
|
|
return 42
|
|
"""
|
|
marked = clean.replace(
|
|
" def test_value",
|
|
" @unittest.skip # TODO: RUSTPYTHON; original guard\n def test_value",
|
|
)
|
|
patches = extract_patches(marked)
|
|
self.assertEqual(
|
|
patches["Owner"]["test_value"],
|
|
[PatchSpec(UtMethod.Skip, reason="original guard")],
|
|
)
|
|
result = apply_patches(clean, patches)
|
|
self.assertIn('@unittest.skip("TODO: RUSTPYTHON; original guard")', result)
|
|
self.assertEqual(apply_patches(clean, extract_patches(result)), result)
|
|
method = list(iter_tests(ast.parse(result)))[0][1]
|
|
self.assertEqual(ast.dump(method.body[0]), "Return(value=Constant(value=42))")
|
|
|
|
def test_unmarked_bare_skip_and_unrelated_decorators_are_not_patches(self):
|
|
code = """class Owner:
|
|
@unittest.skip
|
|
def test_unmarked(self):
|
|
pass
|
|
@other.skip # TODO: RUSTPYTHON; unrelated
|
|
def test_other(self):
|
|
pass
|
|
@unittest.other # TODO: RUSTPYTHON; unrelated
|
|
def test_unknown(self):
|
|
pass
|
|
"""
|
|
self.assertEqual(extract_patches(code), {})
|
|
self.assertEqual(apply_patches(code, {}), code)
|
|
|
|
def test_extract_expected_failure(self):
|
|
"""Test extracting @unittest.expectedFailure decorator."""
|
|
code = f"""
|
|
class TestFoo(unittest.TestCase):
|
|
# {COMMENT}
|
|
@unittest.expectedFailure
|
|
def test_one(self):
|
|
pass
|
|
"""
|
|
patches = extract_patches(code)
|
|
self.assertIn("TestFoo", patches)
|
|
self.assertIn("test_one", patches["TestFoo"])
|
|
specs = patches["TestFoo"]["test_one"]
|
|
self.assertEqual(len(specs), 1)
|
|
self.assertEqual(specs[0].ut_method, UtMethod.ExpectedFailure)
|
|
|
|
def test_extract_expected_failure_inline_comment(self):
|
|
"""Test extracting expectedFailure with inline comment."""
|
|
code = f"""
|
|
class TestFoo(unittest.TestCase):
|
|
@unittest.expectedFailure # {COMMENT}
|
|
def test_one(self):
|
|
pass
|
|
"""
|
|
patches = extract_patches(code)
|
|
self.assertIn("TestFoo", patches)
|
|
self.assertIn("test_one", patches["TestFoo"])
|
|
|
|
def test_extract_skip_with_reason(self):
|
|
"""Test extracting @unittest.skip with reason."""
|
|
code = f'''
|
|
class TestFoo(unittest.TestCase):
|
|
@unittest.skip("{COMMENT}; not implemented")
|
|
def test_one(self):
|
|
pass
|
|
'''
|
|
patches = extract_patches(code)
|
|
self.assertIn("TestFoo", patches)
|
|
specs = patches["TestFoo"]["test_one"]
|
|
self.assertEqual(specs[0].ut_method, UtMethod.Skip)
|
|
self.assertIn("not implemented", specs[0].reason)
|
|
|
|
def test_extract_skip_if(self):
|
|
"""Test extracting @unittest.skipIf decorator."""
|
|
code = f'''
|
|
class TestFoo(unittest.TestCase):
|
|
@unittest.skipIf(sys.platform == "win32", "{COMMENT}; windows issue")
|
|
def test_one(self):
|
|
pass
|
|
'''
|
|
patches = extract_patches(code)
|
|
specs = patches["TestFoo"]["test_one"]
|
|
self.assertEqual(specs[0].ut_method, UtMethod.SkipIf)
|
|
# ast.unparse normalizes quotes to single quotes
|
|
self.assertIn("sys.platform", specs[0].cond)
|
|
self.assertIn("win32", specs[0].cond)
|
|
|
|
def test_no_patches_without_comment(self):
|
|
"""Test that decorators without COMMENT are not extracted."""
|
|
code = """
|
|
class TestFoo(unittest.TestCase):
|
|
@unittest.expectedFailure
|
|
def test_one(self):
|
|
pass
|
|
"""
|
|
patches = extract_patches(code)
|
|
self.assertEqual(patches, {})
|
|
|
|
def test_multiple_patches_same_method(self):
|
|
"""Test extracting multiple decorators on same method."""
|
|
code = f'''
|
|
class TestFoo(unittest.TestCase):
|
|
# {COMMENT}
|
|
@unittest.expectedFailure
|
|
@unittest.skip("{COMMENT}; reason")
|
|
def test_one(self):
|
|
pass
|
|
'''
|
|
patches = extract_patches(code)
|
|
specs = patches["TestFoo"]["test_one"]
|
|
self.assertEqual(len(specs), 2)
|
|
|
|
def test_comment_confusion(self):
|
|
"""
|
|
Test that we only extract our patches when CPython set one of the UT methods,
|
|
that we search for
|
|
"""
|
|
|
|
code = f"""
|
|
class TestFoo(unittest.TestCase):
|
|
@unittest.expectedSuccess # {COMMENT}; reason
|
|
@unittest.expectedFailure
|
|
def test_one(self):
|
|
pass
|
|
"""
|
|
|
|
patches = extract_patches(code)
|
|
specs = patches["TestFoo"]["test_one"]
|
|
self.assertEqual(len(specs), 1)
|
|
|
|
spec = specs[0]
|
|
self.assertEqual(
|
|
spec,
|
|
PatchSpec(ut_method=UtMethod.ExpectedSuccess, cond=None, reason="reason"),
|
|
)
|
|
|
|
|
|
class TestApplyPatches(unittest.TestCase):
|
|
"""Tests for apply_patches function."""
|
|
|
|
def test_conditional_class_existing_method(self):
|
|
code = """import unittest
|
|
if enabled:
|
|
class TestConditional:
|
|
def test_value(self):
|
|
assert 1 + 1 == 2
|
|
"""
|
|
result = apply_patches(
|
|
code,
|
|
{
|
|
"TestConditional": {
|
|
"test_value": [
|
|
PatchSpec(
|
|
UtMethod.ExpectedFailureIf, "condition", "known limitation"
|
|
)
|
|
]
|
|
}
|
|
},
|
|
)
|
|
original = list(iter_tests(ast.parse(code)))[0][1]
|
|
updated = list(iter_tests(ast.parse(result)))[0][1]
|
|
self.assertEqual(ast.dump(original.body[0]), ast.dump(updated.body[0]))
|
|
self.assertEqual(updated.col_offset, 8)
|
|
self.assertIn(" @unittest.expectedFailureIf", result)
|
|
|
|
def test_conditional_class_inherited_override(self):
|
|
code = """import unittest
|
|
class Parent:
|
|
async def test_value(self):
|
|
return 42
|
|
if enabled:
|
|
class Child(Parent):
|
|
pass
|
|
"""
|
|
result = apply_patches(
|
|
code, {"Child": {"test_value": [PatchSpec(UtMethod.ExpectedSuccess)]}}
|
|
)
|
|
methods = list(iter_tests(ast.parse(result)))
|
|
self.assertEqual([cls.name for cls, _ in methods], ["Parent", "Child"])
|
|
self.assertIsInstance(methods[1][1], ast.AsyncFunctionDef)
|
|
self.assertIn(" return await super().test_value()", result)
|
|
self.assertIn(" @unittest.expectedSuccess", result)
|
|
|
|
def test_duplicate_conditional_class_names_are_rejected(self):
|
|
code = """if enabled:
|
|
class Duplicate:
|
|
pass
|
|
else:
|
|
class Duplicate:
|
|
pass
|
|
"""
|
|
with self.assertRaisesRegex(ValueError, "Ambiguous.*Duplicate"):
|
|
apply_patches(
|
|
code,
|
|
{"Duplicate": {"test_value": [PatchSpec(UtMethod.ExpectedFailure)]}},
|
|
)
|
|
|
|
def test_unpatched_conditional_helpers_are_allowed(self):
|
|
code = """if enabled:
|
|
class Helper:
|
|
pass
|
|
else:
|
|
class Helper:
|
|
pass
|
|
class Target:
|
|
helper = Helper
|
|
def test_value(self):
|
|
return 1
|
|
"""
|
|
result = apply_patches(
|
|
code, {"Target": {"test_value": [PatchSpec(UtMethod.ExpectedFailure)]}}
|
|
)
|
|
self.assertEqual(result.count("class Helper:"), 2)
|
|
self.assertEqual(result.count("@unittest.expectedFailure"), 1)
|
|
|
|
def test_ambiguous_transitive_ancestor_is_rejected(self):
|
|
code = """if enabled:
|
|
class Ancestor:
|
|
async def test_value(self):
|
|
pass
|
|
else:
|
|
class Ancestor:
|
|
def test_value(self):
|
|
pass
|
|
class Middle(Ancestor):
|
|
pass
|
|
class Target(Middle):
|
|
pass
|
|
"""
|
|
with self.assertRaisesRegex(ValueError, "Ambiguous.*Ancestor"):
|
|
apply_patches(
|
|
code, {"Target": {"test_value": [PatchSpec(UtMethod.ExpectedFailure)]}}
|
|
)
|
|
|
|
def test_local_class_names_do_not_shadow_module_class(self):
|
|
code = """import unittest
|
|
class Target:
|
|
def test_value(self):
|
|
return 1
|
|
class Container:
|
|
class Target:
|
|
def test_value(self):
|
|
return 2
|
|
def factory():
|
|
class Target:
|
|
def test_value(self):
|
|
return 3
|
|
"""
|
|
result = apply_patches(
|
|
code, {"Target": {"test_value": [PatchSpec(UtMethod.ExpectedFailure)]}}
|
|
)
|
|
tree = ast.parse(result)
|
|
targets = [
|
|
node
|
|
for node in ast.walk(tree)
|
|
if isinstance(node, ast.ClassDef) and node.name == "Target"
|
|
]
|
|
self.assertEqual(
|
|
[len(node.body[0].decorator_list) for node in targets], [1, 0, 0]
|
|
)
|
|
|
|
def test_duplicate_conditional_method_names_are_rejected(self):
|
|
code = """class Owner:
|
|
if enabled:
|
|
def test_value(self):
|
|
return 1
|
|
else:
|
|
def test_value(self):
|
|
return 2
|
|
"""
|
|
with self.assertRaisesRegex(
|
|
ValueError, "Ambiguous method name: Owner.test_value"
|
|
):
|
|
apply_patches(
|
|
code, {"Owner": {"test_value": [PatchSpec(UtMethod.ExpectedFailure)]}}
|
|
)
|
|
|
|
def test_apply_expected_failure(self):
|
|
"""Test applying @unittest.expectedFailure."""
|
|
code = """import unittest
|
|
|
|
class TestFoo(unittest.TestCase):
|
|
def test_one(self):
|
|
pass
|
|
"""
|
|
patches = {
|
|
"TestFoo": {"test_one": [PatchSpec(UtMethod.ExpectedFailure, None, "")]}
|
|
}
|
|
result = apply_patches(code, patches)
|
|
self.assertIn("@unittest.expectedFailure", result)
|
|
self.assertIn(COMMENT, result)
|
|
|
|
def test_apply_skip_with_reason(self):
|
|
"""Test applying @unittest.skip with reason."""
|
|
code = """import unittest
|
|
|
|
class TestFoo(unittest.TestCase):
|
|
def test_one(self):
|
|
pass
|
|
"""
|
|
patches = {
|
|
"TestFoo": {"test_one": [PatchSpec(UtMethod.Skip, None, "not ready")]}
|
|
}
|
|
result = apply_patches(code, patches)
|
|
self.assertIn("@unittest.skip", result)
|
|
self.assertIn("not ready", result)
|
|
|
|
def test_apply_skip_if(self):
|
|
"""Test applying @unittest.skipIf."""
|
|
code = """import unittest
|
|
|
|
class TestFoo(unittest.TestCase):
|
|
def test_one(self):
|
|
pass
|
|
"""
|
|
patches = {
|
|
"TestFoo": {
|
|
"test_one": [
|
|
PatchSpec(UtMethod.SkipIf, "sys.platform == 'win32'", "windows")
|
|
]
|
|
}
|
|
}
|
|
result = apply_patches(code, patches)
|
|
self.assertIn("@unittest.skipIf", result)
|
|
self.assertIn('sys.platform == "win32"', result)
|
|
|
|
def test_apply_preserves_existing_decorators(self):
|
|
"""Test that existing decorators are preserved."""
|
|
code = """import unittest
|
|
|
|
class TestFoo(unittest.TestCase):
|
|
@some_decorator
|
|
def test_one(self):
|
|
pass
|
|
"""
|
|
patches = {
|
|
"TestFoo": {"test_one": [PatchSpec(UtMethod.ExpectedFailure, None, "")]}
|
|
}
|
|
result = apply_patches(code, patches)
|
|
self.assertIn("@some_decorator", result)
|
|
self.assertIn("@unittest.expectedFailure", result)
|
|
|
|
def test_inherited_override_expands_inline_class(self):
|
|
for source in (
|
|
"class Child(Base): pass # keep comment\n",
|
|
"class Child(Base): value = 1; other = 2\n",
|
|
"class Child(\n Base\n): pass\n",
|
|
"if True:\n class Child(Base): pass\n",
|
|
"class Café(Base): pass\n",
|
|
):
|
|
with self.subTest(source=source):
|
|
name = "Café" if "Café" in source else "Child"
|
|
patches = {
|
|
name: {
|
|
"test_parent": [PatchSpec(UtMethod.ExpectedFailure, None, "")]
|
|
}
|
|
}
|
|
before = ast.parse(source)
|
|
result = apply_patches(source, patches)
|
|
after = ast.parse(result)
|
|
old_class = next(
|
|
node for node in ast.walk(before) if isinstance(node, ast.ClassDef)
|
|
)
|
|
new_class = next(
|
|
node for node in ast.walk(after) if isinstance(node, ast.ClassDef)
|
|
)
|
|
self.assertEqual(
|
|
[ast.dump(node) for node in old_class.body],
|
|
[ast.dump(node) for node in new_class.body[:-1]],
|
|
)
|
|
self.assertEqual(new_class.body[-1].name, "test_parent")
|
|
self.assertIn("return super().test_parent()", result)
|
|
if "# keep comment" in source:
|
|
self.assertIn("# keep comment", result)
|
|
|
|
def test_apply_inherited_method(self):
|
|
"""Test applying patch to inherited method (creates override)."""
|
|
code = """import unittest
|
|
|
|
class TestFoo(unittest.TestCase):
|
|
pass
|
|
"""
|
|
patches = {
|
|
"TestFoo": {
|
|
"test_inherited": [PatchSpec(UtMethod.ExpectedFailure, None, "")]
|
|
}
|
|
}
|
|
result = apply_patches(code, patches)
|
|
self.assertIn("def test_inherited(self):", result)
|
|
self.assertIn("return super().test_inherited()", result)
|
|
|
|
def test_apply_adds_unittest_import(self):
|
|
"""Test that unittest import is added if missing."""
|
|
code = """import sys
|
|
|
|
class TestFoo:
|
|
def test_one(self):
|
|
pass
|
|
"""
|
|
patches = {
|
|
"TestFoo": {"test_one": [PatchSpec(UtMethod.ExpectedFailure, None, "")]}
|
|
}
|
|
result = apply_patches(code, patches)
|
|
# Should add unittest import after existing imports
|
|
self.assertIn("import unittest", result)
|
|
|
|
def test_apply_no_duplicate_import(self):
|
|
"""Test that unittest import is not duplicated."""
|
|
code = """import unittest
|
|
|
|
class TestFoo(unittest.TestCase):
|
|
def test_one(self):
|
|
pass
|
|
"""
|
|
patches = {
|
|
"TestFoo": {"test_one": [PatchSpec(UtMethod.ExpectedFailure, None, "")]}
|
|
}
|
|
result = apply_patches(code, patches)
|
|
# Count occurrences of 'import unittest'
|
|
count = result.count("import unittest")
|
|
self.assertEqual(count, 1)
|
|
|
|
|
|
class TestPatchSpec(unittest.TestCase):
|
|
"""Tests for PatchSpec class."""
|
|
|
|
def test_as_decorator_expected_failure(self):
|
|
"""Test generating expectedFailure decorator string."""
|
|
spec = PatchSpec(UtMethod.ExpectedFailure, None, "reason")
|
|
decorator = spec.as_decorator()
|
|
self.assertIn("@unittest.expectedFailure", decorator)
|
|
self.assertIn(COMMENT, decorator)
|
|
self.assertIn("reason", decorator)
|
|
|
|
def test_as_decorator_skip(self):
|
|
"""Test generating skip decorator string."""
|
|
spec = PatchSpec(UtMethod.Skip, None, "not ready")
|
|
decorator = spec.as_decorator()
|
|
self.assertIn("@unittest.skip", decorator)
|
|
self.assertIn("not ready", decorator)
|
|
|
|
def test_as_decorator_skip_if(self):
|
|
"""Test generating skipIf decorator string."""
|
|
spec = PatchSpec(UtMethod.SkipIf, "condition", "reason")
|
|
decorator = spec.as_decorator()
|
|
self.assertIn("@unittest.skipIf", decorator)
|
|
self.assertIn("condition", decorator)
|
|
|
|
|
|
class TestRoundTrip(unittest.TestCase):
|
|
"""Tests for extract -> apply round trip."""
|
|
|
|
def test_round_trip_expected_failure(self):
|
|
"""Test that extracted patches can be re-applied."""
|
|
original = f"""import unittest
|
|
|
|
class TestFoo(unittest.TestCase):
|
|
# {COMMENT}
|
|
@unittest.expectedFailure
|
|
def test_one(self):
|
|
pass
|
|
"""
|
|
# Extract patches
|
|
patches = extract_patches(original)
|
|
|
|
# Apply to clean code
|
|
clean = """import unittest
|
|
|
|
class TestFoo(unittest.TestCase):
|
|
def test_one(self):
|
|
pass
|
|
"""
|
|
result = apply_patches(clean, patches)
|
|
|
|
# Should have the decorator
|
|
self.assertIn("@unittest.expectedFailure", result)
|
|
self.assertIn(COMMENT, result)
|
|
|
|
def test_round_trip_with_patches_on_class(self):
|
|
"""Test that extracted patches can be re-applied."""
|
|
original = f"""import unittest
|
|
|
|
@unittest.skipIf(a == b, "{COMMENT}")
|
|
@unittest.expectedFailure # {COMMENT}
|
|
class TestFoo(unittest.TestCase):
|
|
...
|
|
"""
|
|
# Extract patches
|
|
patches = extract_patches(original)
|
|
|
|
# Apply to clean code
|
|
clean = """import unittest
|
|
|
|
class TestFoo(unittest.TestCase):
|
|
def test_one(self):
|
|
pass
|
|
"""
|
|
result = apply_patches(clean, patches)
|
|
|
|
# Should have the decorator
|
|
self.assertIn("@unittest.expectedFailure", result)
|
|
self.assertIn("@unittest.skipIf", result)
|
|
self.assertIn(COMMENT, result)
|
|
|
|
|
|
class TestFindImportInsertLine(unittest.TestCase):
|
|
"""Tests for _find_import_insert_line function."""
|
|
|
|
def test_with_imports(self):
|
|
"""Test finding line after imports."""
|
|
code = """import os
|
|
import sys
|
|
|
|
class Foo:
|
|
pass
|
|
"""
|
|
tree = ast.parse(code)
|
|
line = _find_import_insert_line(tree)
|
|
self.assertEqual(line, 2)
|
|
|
|
def test_no_imports_with_docstring(self):
|
|
"""Test fallback to after docstring when no imports."""
|
|
code = '''"""Module docstring."""
|
|
|
|
class Foo:
|
|
pass
|
|
'''
|
|
tree = ast.parse(code)
|
|
line = _find_import_insert_line(tree)
|
|
self.assertEqual(line, 1)
|
|
|
|
def test_no_imports_no_docstring(self):
|
|
"""Test fallback to line 0 when no imports and no docstring."""
|
|
code = """class Foo:
|
|
pass
|
|
"""
|
|
tree = ast.parse(code)
|
|
line = _find_import_insert_line(tree)
|
|
self.assertEqual(line, 0)
|
|
|
|
|
|
DOCTEST_SOURCE = "raise X, Y\n"
|
|
DOCTEST_METADATA = {DOCTEST_SOURCE: "known mismatch"}
|
|
DOCTEST_OLD = (
|
|
'''"""
|
|
>>> raise X, Y # TODO: RUSTPYTHON; old reason # doctest: +EXPECTED_FAILURE
|
|
Traceback (most recent call last):
|
|
SyntaxError: old message
|
|
"""
|
|
'''
|
|
+ PATCHED_LOAD_TESTS
|
|
+ "\n"
|
|
)
|
|
DOCTEST_CANONICAL = (
|
|
'''"""
|
|
>>> raise X, Y
|
|
Traceback (most recent call last):
|
|
SyntaxError: final message
|
|
"""
|
|
'''
|
|
+ CANONICAL_LOAD_TESTS
|
|
+ "\n"
|
|
)
|
|
|
|
|
|
class ExactDoctestTests(unittest.TestCase):
|
|
def test_extracts_only_existing_guards(self):
|
|
self.assertEqual(set(extract_doctest_guards(DOCTEST_OLD)), {DOCTEST_SOURCE})
|
|
self.assertEqual(extract_doctest_guards(DOCTEST_CANONICAL), {})
|
|
self.assertEqual(extract_doctest_guards("value = 1\n"), {})
|
|
|
|
def test_duplicate_existing_guards_are_rejected(self):
|
|
duplicate = DOCTEST_OLD.replace(
|
|
">>> raise X, Y # TODO:",
|
|
">>> raise X, Y # TODO: RUSTPYTHON; duplicate # doctest: +EXPECTED_FAILURE\n"
|
|
">>> raise X, Y # TODO:",
|
|
)
|
|
with self.assertRaises(ValueError):
|
|
extract_doctest_guards(duplicate)
|
|
|
|
def test_preserves_final_output_and_roundtrips(self):
|
|
result = apply_doctest_metadata(
|
|
DOCTEST_OLD, DOCTEST_CANONICAL, DOCTEST_METADATA
|
|
)
|
|
self.assertIn("SyntaxError: final message", result)
|
|
self.assertNotIn("old message", result)
|
|
self.assertEqual(result.count("doctest: +EXPECTED_FAILURE"), 1)
|
|
self.assertIn(PATCHED_LOAD_TESTS, result)
|
|
self.assertEqual(
|
|
apply_doctest_metadata(result, DOCTEST_CANONICAL, DOCTEST_METADATA), result
|
|
)
|
|
|
|
def test_duplicate_input_is_rejected(self):
|
|
duplicate = DOCTEST_CANONICAL.replace(
|
|
">>> raise X, Y\n", ">>> raise X, Y\n>>> raise X, Y\n"
|
|
)
|
|
with self.assertRaises(ValueError):
|
|
apply_doctest_metadata(DOCTEST_OLD, duplicate, DOCTEST_METADATA)
|
|
|
|
def test_missing_canonical_or_old_case_is_rejected(self):
|
|
with self.assertRaises(ValueError):
|
|
apply_doctest_metadata(
|
|
DOCTEST_OLD,
|
|
DOCTEST_CANONICAL.replace("raise X, Y", "raise X"),
|
|
DOCTEST_METADATA,
|
|
)
|
|
with self.assertRaises(ValueError):
|
|
apply_doctest_metadata(
|
|
DOCTEST_OLD.replace("raise X, Y", "raise X"),
|
|
DOCTEST_CANONICAL,
|
|
DOCTEST_METADATA,
|
|
)
|
|
|
|
def test_changed_loader_and_unmarked_old_case_are_rejected(self):
|
|
with self.assertRaises(ValueError):
|
|
apply_doctest_metadata(
|
|
DOCTEST_OLD,
|
|
DOCTEST_CANONICAL.replace("DocTestSuite()", "DocTestSuite(None)"),
|
|
DOCTEST_METADATA,
|
|
)
|
|
with self.assertRaises(ValueError):
|
|
apply_doctest_metadata(
|
|
DOCTEST_CANONICAL, DOCTEST_CANONICAL, DOCTEST_METADATA
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|