-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmutation_test.py
More file actions
156 lines (122 loc) · 4.77 KB
/
mutation_test.py
File metadata and controls
156 lines (122 loc) · 4.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
"""Simple mutation testing for Lua files.
This script mutates a variety of operators in the project's Lua business logic
files and runs the Lua test suite after each mutation. It reports whether the
tests catch the mutation ("KILLED") or not ("SURVIVED").
Running this script requires a `lua` interpreter installed and assumes the test
suite can be executed with `lua my_test_suite.lua`.
"""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
from typing import Iterable, Tuple, List
from luaparser import ast
from luaparser import astnodes
import contextlib
import io
TEST_COMMAND = ["lua", "my_test_suite.lua"]
# All Lua files that contain business logic we want to mutate
FILES_TO_MUTATE: Iterable[Path] = [
Path("evens.lua"),
Path("logic/init.lua"),
Path("modutils/init.lua"),
Path("utils/init.lua"),
]
# Mapping of operator nodes to their mutated counterparts
OPERATOR_MUTATIONS = {
astnodes.EqToOp: astnodes.NotEqToOp,
astnodes.NotEqToOp: astnodes.EqToOp,
astnodes.GreaterThanOp: astnodes.LessOrEqThanOp,
astnodes.LessOrEqThanOp: astnodes.GreaterThanOp,
astnodes.LessThanOp: astnodes.GreaterOrEqThanOp,
astnodes.GreaterOrEqThanOp: astnodes.LessThanOp,
astnodes.AndLoOp: astnodes.OrLoOp,
astnodes.OrLoOp: astnodes.AndLoOp,
astnodes.AddOp: astnodes.SubOp,
astnodes.SubOp: astnodes.AddOp,
astnodes.MultOp: astnodes.FloatDivOp,
astnodes.FloatDivOp: astnodes.MultOp,
astnodes.ModOp: astnodes.FloorDivOp,
astnodes.FloorDivOp: astnodes.ModOp,
}
def parse_no_output(source: str) -> astnodes.Chunk:
"""Parse Lua source without printing the AST."""
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
return ast.parse(source)
def collect_operator_nodes(tree: astnodes.Chunk) -> list[astnodes.Node]:
"""Return all operator nodes we want to mutate in the AST."""
nodes = []
for node in ast.walk(tree):
if isinstance(node, tuple(OPERATOR_MUTATIONS.keys())):
nodes.append(node)
return nodes
def mutate_operator(node: astnodes.Node) -> None:
"""Mutate the given operator node in place."""
original_cls = node.__class__
mutated_cls = OPERATOR_MUTATIONS.get(original_cls)
if mutated_cls is None:
return
if hasattr(node, "left") and hasattr(node, "right"):
mutated = mutated_cls(left=node.left, right=node.right)
else:
mutated = mutated_cls(operand=node.operand) # type: ignore[arg-type]
node.__class__ = mutated_cls
node.__dict__.update(mutated.__dict__)
def get_position(node: astnodes.Node) -> tuple[int, int]:
"""Return (line, column) for the given AST node."""
if hasattr(node, "_first_token"):
token = node._first_token
return getattr(token, "line", -1), getattr(token, "column", -1)
return -1, -1
def run_tests() -> bool:
"""Run the Lua test suite and return True if tests pass."""
result = subprocess.run(TEST_COMMAND, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
sys.stdout.buffer.write(result.stdout)
return result.returncode == 0
def mutate_file(path: Path) -> Tuple[int, int, List[str]]:
"""Mutate the given file and return a tuple of (killed, survived, surviving details)."""
killed = 0
survived = 0
surviving_details: List[str] = []
original_code = path.read_text()
tree = parse_no_output(original_code)
operators = collect_operator_nodes(tree)
for idx in range(len(operators)):
tree = parse_no_output(original_code)
operators = collect_operator_nodes(tree)
node = operators[idx]
line, col = get_position(node)
original_name = node.__class__.__name__
mutated_name = OPERATOR_MUTATIONS[node.__class__].__name__
mutate_operator(node)
path.write_text(ast.to_lua_source(tree))
if run_tests():
survived += 1
status = "SURVIVED"
surviving_details.append(f"{path}:{line}:{col} {original_name}->{mutated_name}")
else:
killed += 1
status = "KILLED"
print(f"{path} mutation {idx + 1}/{len(operators)}: {status}")
# Restore original
path.write_text(original_code)
return killed, survived, surviving_details
def main() -> None:
total_killed = 0
total_survived = 0
all_surviving: List[str] = []
for file_path in FILES_TO_MUTATE:
if not file_path.exists():
continue
killed, survived, details = mutate_file(file_path)
total_killed += killed
total_survived += survived
all_surviving.extend(details)
print(f"Killed: {total_killed}, Survived: {total_survived}")
if all_surviving:
print("\nSurviving mutants:")
for d in all_surviving:
print(f" {d}")
if __name__ == "__main__":
main()