Skip to content

Commit 03fcb6f

Browse files
committed
fix(clone): remove internal test clone groups with shared helpers and parametrized CFG/normalize checks
1 parent eefa10d commit 03fcb6f

5 files changed

Lines changed: 213 additions & 226 deletions

File tree

codeclone.baseline.json

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
{
2-
"functions": [
3-
"056967a5e5569762522015b120810353dc84986a|20-49",
4-
"2482e4ffa6c3c349be9626d4a95abbbb57193345|20-49",
5-
"54b7b79d1ff3384fb96b3dd97944d7b67990b3f3|0-19",
6-
"554bfacfa9bf1565bd2f5ea36e89b6efaee29c2d|0-19",
7-
"b0070927d98fa8274982caef45107f4e6b4d6fef|0-19",
8-
"c8e7da40a2dc106d1aa092044f88f01d9abae054|0-19"
9-
],
2+
"functions": [],
103
"blocks": [],
114
"python_version": "3.13",
125
"baseline_version": "1.3.0",

pyproject.toml

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ dev = [
6565
"build>=1.2.0",
6666
"twine>=5.0.0",
6767
"mypy>=1.19.1",
68-
"ruff>=0.12.0",
68+
"ruff>=0.15.0",
69+
"pre-commit>=4.5.1",
6970
]
7071

7172
[project.scripts]
@@ -107,8 +108,3 @@ select = ["E", "F", "W", "I", "B", "UP", "SIM", "C4", "PIE", "PERF", "RUF"]
107108
quote-style = "double"
108109
indent-style = "space"
109110
line-ending = "auto"
110-
111-
[dependency-groups]
112-
dev = [
113-
"pre-commit>=4.5.1",
114-
]

tests/test_cfg.py

Lines changed: 125 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from codeclone.cfg import CFG, CFGBuilder
77
from codeclone.cfg_model import CFG as CFGModel
8+
from codeclone.cfg_model import Block
89
from codeclone.extractor import get_cfg_fingerprint
910
from codeclone.meta_markers import CFG_META_PREFIX
1011
from codeclone.normalize import NormalizationConfig
@@ -44,6 +45,47 @@ def _const_meta_value(stmt: ast.stmt) -> str | None:
4445
return stmt.value.id
4546

4647

48+
def _parse_function(
49+
source: str, *, skip_reason: str | None = None
50+
) -> ast.FunctionDef | ast.AsyncFunctionDef:
51+
try:
52+
module = ast.parse(dedent(source))
53+
except SyntaxError:
54+
if skip_reason:
55+
pytest.skip(skip_reason)
56+
raise
57+
for node in ast.walk(module):
58+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
59+
return node
60+
raise AssertionError("Expected at least one function in source")
61+
62+
63+
def _cfg_fingerprint(
64+
source: str, qualname: str, *, skip_reason: str | None = None
65+
) -> str:
66+
func = _parse_function(source, skip_reason=skip_reason)
67+
cfg = NormalizationConfig()
68+
return get_cfg_fingerprint(func, cfg, qualname)
69+
70+
71+
def _assert_fingerprint_diff(
72+
source_a: str, source_b: str, *, skip_reason: str | None = None
73+
) -> None:
74+
fp_a = _cfg_fingerprint(source_a, "m:f", skip_reason=skip_reason)
75+
fp_b = _cfg_fingerprint(source_b, "m:g", skip_reason=skip_reason)
76+
assert fp_a != fp_b
77+
78+
79+
def _single_return_block(cfg: CFG) -> Block:
80+
return_blocks = [
81+
block
82+
for block in cfg.blocks
83+
if any(isinstance(stmt, ast.Return) for stmt in block.statements)
84+
]
85+
assert len(return_blocks) == 1
86+
return return_blocks[0]
87+
88+
4789
def test_cfg_if_else() -> None:
4890
source = """
4991
def f(a):
@@ -581,178 +623,137 @@ def f(x):
581623
assert "MatchMapping" in patterns_found[1]
582624

583625

584-
def test_cfg_match_guard_affects_fingerprint() -> None:
585-
code_with_guard = """
626+
@pytest.mark.parametrize(
627+
("source_a", "source_b", "skip_reason"),
628+
[
629+
(
630+
"""
586631
def f(x):
587632
match x:
588633
case 1 if cond():
589634
return 1
590635
case _:
591636
return 2
592-
"""
593-
code_without_guard = """
637+
""",
638+
"""
594639
def f(x):
595640
match x:
596641
case 1:
597642
return 1
598643
case _:
599644
return 2
600-
"""
601-
try:
602-
func_with_guard = ast.parse(dedent(code_with_guard)).body[0]
603-
func_without_guard = ast.parse(dedent(code_without_guard)).body[0]
604-
except SyntaxError:
605-
pytest.skip("Match syntax is unavailable")
606-
607-
assert isinstance(func_with_guard, (ast.FunctionDef, ast.AsyncFunctionDef))
608-
assert isinstance(func_without_guard, (ast.FunctionDef, ast.AsyncFunctionDef))
609-
cfg = NormalizationConfig()
610-
fp_with_guard = get_cfg_fingerprint(func_with_guard, cfg, "m:f")
611-
fp_without_guard = get_cfg_fingerprint(func_without_guard, cfg, "m:g")
612-
assert fp_with_guard != fp_without_guard
613-
614-
615-
def test_cfg_match_case_order_affects_fingerprint() -> None:
616-
source_a = """
645+
""",
646+
"Match syntax is unavailable",
647+
),
648+
(
649+
"""
617650
def f(x):
618651
match x:
619652
case 1:
620653
return 1
621654
case _:
622655
return 2
623-
"""
624-
source_b = """
656+
""",
657+
"""
625658
def g(x):
626659
match x:
627660
case _:
628661
return 2
629662
case 1:
630663
return 1
631-
"""
632-
try:
633-
func_a = ast.parse(dedent(source_a)).body[0]
634-
func_b = ast.parse(dedent(source_b)).body[0]
635-
except SyntaxError:
636-
pytest.skip("Match syntax is unavailable")
637-
assert isinstance(func_a, (ast.FunctionDef, ast.AsyncFunctionDef))
638-
assert isinstance(func_b, (ast.FunctionDef, ast.AsyncFunctionDef))
639-
cfg = NormalizationConfig()
640-
fp_a = get_cfg_fingerprint(func_a, cfg, "m:f")
641-
fp_b = get_cfg_fingerprint(func_b, cfg, "m:g")
642-
assert fp_a != fp_b
643-
644-
645-
def test_cfg_try_handler_order_affects_fingerprint() -> None:
646-
source_a = """
664+
""",
665+
"Match syntax is unavailable",
666+
),
667+
(
668+
"""
647669
def f(x):
648670
try:
649671
return risky(x)
650672
except ValueError:
651673
return 1
652674
except Exception:
653675
return 2
654-
"""
655-
source_b = """
676+
""",
677+
"""
656678
def g(x):
657679
try:
658680
return risky(x)
659681
except Exception:
660682
return 2
661683
except ValueError:
662684
return 1
663-
"""
664-
func_a = ast.parse(dedent(source_a)).body[0]
665-
func_b = ast.parse(dedent(source_b)).body[0]
666-
assert isinstance(func_a, (ast.FunctionDef, ast.AsyncFunctionDef))
667-
assert isinstance(func_b, (ast.FunctionDef, ast.AsyncFunctionDef))
668-
cfg = NormalizationConfig()
669-
fp_a = get_cfg_fingerprint(func_a, cfg, "m:f")
670-
fp_b = get_cfg_fingerprint(func_b, cfg, "m:g")
671-
assert fp_a != fp_b
672-
673-
674-
def test_cfg_for_else_affects_fingerprint() -> None:
675-
with_else = """
685+
""",
686+
None,
687+
),
688+
(
689+
"""
676690
def f(xs):
677691
for x in xs:
678692
pass
679693
else:
680694
y = 1
681-
"""
682-
without_else = """
695+
""",
696+
"""
683697
def f(xs):
684698
for x in xs:
685699
pass
686-
"""
687-
func_with_else = ast.parse(dedent(with_else)).body[0]
688-
func_without_else = ast.parse(dedent(without_else)).body[0]
689-
assert isinstance(func_with_else, (ast.FunctionDef, ast.AsyncFunctionDef))
690-
assert isinstance(func_without_else, (ast.FunctionDef, ast.AsyncFunctionDef))
691-
cfg = NormalizationConfig()
692-
fp_with_else = get_cfg_fingerprint(func_with_else, cfg, "m:f")
693-
fp_without_else = get_cfg_fingerprint(func_without_else, cfg, "m:g")
694-
assert fp_with_else != fp_without_else
695-
696-
697-
def test_cfg_while_else_affects_fingerprint() -> None:
698-
with_else = """
700+
""",
701+
None,
702+
),
703+
(
704+
"""
699705
def f(flag):
700706
while flag:
701707
flag = False
702708
else:
703709
x = 1
704-
"""
705-
without_else = """
710+
""",
711+
"""
706712
def f(flag):
707713
while flag:
708714
flag = False
709-
"""
710-
func_with_else = ast.parse(dedent(with_else)).body[0]
711-
func_without_else = ast.parse(dedent(without_else)).body[0]
712-
assert isinstance(func_with_else, (ast.FunctionDef, ast.AsyncFunctionDef))
713-
assert isinstance(func_without_else, (ast.FunctionDef, ast.AsyncFunctionDef))
714-
cfg = NormalizationConfig()
715-
fp_with_else = get_cfg_fingerprint(func_with_else, cfg, "m:f")
716-
fp_without_else = get_cfg_fingerprint(func_without_else, cfg, "m:g")
717-
assert fp_with_else != fp_without_else
718-
719-
720-
def test_cfg_break_terminates_block() -> None:
721-
source = """
722-
def f(xs):
723-
for x in xs:
724-
break
725-
y = 1
726-
"""
727-
cfg = build_cfg_from_source(source)
728-
break_blocks = [
729-
block
730-
for block in cfg.blocks
731-
if any(isinstance(stmt, ast.Break) for stmt in block.statements)
732-
]
733-
assert len(break_blocks) == 1
734-
break_block = break_blocks[0]
735-
assert break_block.is_terminated is True
736-
assert all(not isinstance(stmt, ast.Assign) for stmt in break_block.statements)
737-
738-
739-
def test_cfg_continue_terminates_block() -> None:
740-
source = """
715+
""",
716+
None,
717+
),
718+
],
719+
ids=[
720+
"match_guard",
721+
"match_case_order",
722+
"try_handler_order",
723+
"for_else",
724+
"while_else",
725+
],
726+
)
727+
def test_cfg_fingerprint_variants(
728+
source_a: str, source_b: str, skip_reason: str | None
729+
) -> None:
730+
_assert_fingerprint_diff(source_a, source_b, skip_reason=skip_reason)
731+
732+
733+
@pytest.mark.parametrize(
734+
("keyword", "stmt_type"),
735+
[("break", ast.Break), ("continue", ast.Continue)],
736+
ids=["break", "continue"],
737+
)
738+
def test_cfg_loop_control_terminates_block(
739+
keyword: str, stmt_type: type[ast.stmt]
740+
) -> None:
741+
source = f"""
741742
def f(xs):
742743
for x in xs:
743-
continue
744+
{keyword}
744745
y = 1
745746
"""
746747
cfg = build_cfg_from_source(source)
747-
continue_blocks = [
748+
control_blocks = [
748749
block
749750
for block in cfg.blocks
750-
if any(isinstance(stmt, ast.Continue) for stmt in block.statements)
751+
if any(isinstance(stmt, stmt_type) for stmt in block.statements)
751752
]
752-
assert len(continue_blocks) == 1
753-
continue_block = continue_blocks[0]
754-
assert continue_block.is_terminated is True
755-
assert all(not isinstance(stmt, ast.Assign) for stmt in continue_block.statements)
753+
assert len(control_blocks) == 1
754+
control_block = control_blocks[0]
755+
assert control_block.is_terminated is True
756+
assert all(not isinstance(stmt, ast.Assign) for stmt in control_block.statements)
756757

757758

758759
def test_cfg_break_skips_for_else_block() -> None:
@@ -783,42 +784,31 @@ def f(xs):
783784
assert else_blocks[0] not in break_blocks[0].successors
784785

785786

786-
def test_cfg_while_else_terminated_branch() -> None:
787-
source = """
787+
@pytest.mark.parametrize(
788+
"source",
789+
[
790+
"""
788791
def f(flag):
789792
while flag:
790793
flag = False
791794
else:
792795
return 1
793-
"""
794-
cfg = build_cfg_from_source(source)
795-
return_blocks = [
796-
block
797-
for block in cfg.blocks
798-
if any(isinstance(stmt, ast.Return) for stmt in block.statements)
799-
]
800-
assert len(return_blocks) == 1
801-
assert return_blocks[0].is_terminated is True
802-
assert cfg.exit in return_blocks[0].successors
803-
804-
805-
def test_cfg_for_else_terminated_branch() -> None:
806-
source = """
796+
""",
797+
"""
807798
def f(xs):
808799
for x in xs:
809800
pass
810801
else:
811802
return 1
812-
"""
803+
""",
804+
],
805+
ids=["while_else", "for_else"],
806+
)
807+
def test_cfg_loop_else_terminated_branch(source: str) -> None:
813808
cfg = build_cfg_from_source(source)
814-
return_blocks = [
815-
block
816-
for block in cfg.blocks
817-
if any(isinstance(stmt, ast.Return) for stmt in block.statements)
818-
]
819-
assert len(return_blocks) == 1
820-
assert return_blocks[0].is_terminated is True
821-
assert cfg.exit in return_blocks[0].successors
809+
return_block = _single_return_block(cfg)
810+
assert return_block.is_terminated is True
811+
assert cfg.exit in return_block.successors
822812

823813

824814
def test_cfg_break_outside_loop_falls_back_to_exit() -> None:

0 commit comments

Comments
 (0)