Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions multiplex/checks/syntactic_equivalence.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,37 @@
import tree_sitter_java as ts_java


def _get_clean_tree(source_code, parser):
tree = parser.parse(source_code.text, encoding="utf-8")
root_node = tree.root_node
def _get_clean_tree(root_node) -> str:
"""Serialize a parse tree to a string for comparison.

def traverse(node):
Comments and whitespace are ignored.
Leaf tokens include their source text to include values in comparison.
"""

def traverse(node) -> str:
if node.type in ("comment", "line_comment", "block_comment", "whitespace"):
return ""
if node.child_count == 0:
return node.type
return node.type + "(" + "".join([traverse(child) for child in node.children]) + ")"
return node.type + "-" + node.text.decode("utf-8")
return node.type + "(" + "".join(traverse(child) for child in node.children) + ")"

return traverse(root_node)


def _tree_is_equivalent(original_root_node, mutant_root_node, parser):
def _tree_is_equivalent(original_root_node, mutant_root_node):
"""Check tree for equivalence."""
mutant = _get_clean_tree(mutant_root_node, parser)
original = _get_clean_tree(original_root_node, parser)
mutant = _get_clean_tree(mutant_root_node)
original = _get_clean_tree(original_root_node)

seq_match = SequenceMatcher(None, original, mutant)
ratio = seq_match.ratio()
print("Syntactic Equivalence Ratio:", ratio)

if mutant == original:
return True
return False
return mutant == original


def _read_file(fn):
"""Read file"""
src = ""
with open(fn, "r", encoding="utf-8") as reader:
src = reader.read()
Expand All @@ -45,4 +47,4 @@ def check_mutant_equivalent(mutant_filename, original_filename) -> bool:
parser = Parser(language)
mutant_tree = parser.parse(_read_file(mutant_filename), encoding="utf-8")
original_tree = parser.parse(_read_file(original_filename), encoding="utf-8")
return _tree_is_equivalent(original_tree.root_node, mutant_tree.root_node, parser)
return _tree_is_equivalent(original_tree.root_node, mutant_tree.root_node)
38 changes: 38 additions & 0 deletions tests/checks/test_syntactic_equivalence.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,41 @@ def test_check_mutant_equivalent_is_equivalent(equivalent_mutant_path, original_
def test_check_mutant_equivalent_not_equivalent(killed_mutant_path, original_method_path):
"""Check mutant identified as not equivalent."""
assert not check_mutant_equivalent(killed_mutant_path, original_method_path)


def _write(tmp_path, name, code):
p = tmp_path / name
p.write_text(code)
return p


def test_string_literal_change_is_not_equivalent(tmp_path):
"""A changed string literal (same AST shape) must not be equivalent."""
original = _write(tmp_path, "o.java", 'String f() { return "negative"; }')
mutant = _write(tmp_path, "m.java", 'String f() { return "positive"; }')
assert not check_mutant_equivalent(mutant, original)


def test_numeric_literal_change_is_not_equivalent(tmp_path):
original = _write(tmp_path, "o.java", "int f() { return 1; }")
mutant = _write(tmp_path, "m.java", "int f() { return 2; }")
assert not check_mutant_equivalent(mutant, original)


def test_identifier_change_is_not_equivalent(tmp_path):
original = _write(tmp_path, "o.java", "int f(int a, int b) { return a; }")
mutant = _write(tmp_path, "m.java", "int f(int a, int b) { return b; }")
assert not check_mutant_equivalent(mutant, original)


def test_comment_only_change_is_equivalent(tmp_path):
"""Differences only in comments/whitespace remain equivalent."""
original = _write(tmp_path, "o.java", "int f() { return 1; }")
mutant = _write(tmp_path, "m.java", "int f() {\n // added comment\n return 1;\n}")
assert check_mutant_equivalent(mutant, original)


def test_identical_source_is_equivalent(tmp_path):
original = _write(tmp_path, "o.java", "int f() { return 1; }")
mutant = _write(tmp_path, "m.java", "int f() { return 1; }")
assert check_mutant_equivalent(mutant, original)
Loading