From 996d14c71aa74525c46489e123e5e4a407ebd3fc Mon Sep 17 00:00:00 2001 From: MgnMtn Date: Fri, 10 Jul 2026 15:16:09 +0100 Subject: [PATCH] fix: include literal values in equivalence checks --- multiplex/checks/syntactic_equivalence.py | 28 ++++++++-------- tests/checks/test_syntactic_equivalence.py | 38 ++++++++++++++++++++++ 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/multiplex/checks/syntactic_equivalence.py b/multiplex/checks/syntactic_equivalence.py index 0063313..24717f0 100644 --- a/multiplex/checks/syntactic_equivalence.py +++ b/multiplex/checks/syntactic_equivalence.py @@ -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() @@ -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) diff --git a/tests/checks/test_syntactic_equivalence.py b/tests/checks/test_syntactic_equivalence.py index 108ed78..0b3b4a2 100644 --- a/tests/checks/test_syntactic_equivalence.py +++ b/tests/checks/test_syntactic_equivalence.py @@ -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)