fable-engine 1.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fable_compressor.py +356 -0
- fable_engine/__init__.py +1 -0
- fable_engine/actions/__init__.py +291 -0
- fable_engine/actions/cas.py +182 -0
- fable_engine/actions/deliberation.py +523 -0
- fable_engine/actions/fleet.py +807 -0
- fable_engine/actions/lifecycle.py +298 -0
- fable_engine/actions/scrapers.py +116 -0
- fable_engine/actions/system3.py +815 -0
- fable_engine/browser.py +824 -0
- fable_engine/cas.py +974 -0
- fable_engine/fable_session.json +510 -0
- fable_engine/guards.py +283 -0
- fable_engine/schema.py +714 -0
- fable_engine/scrapers/__init__.py +32 -0
- fable_engine/scrapers/arxiv.py +115 -0
- fable_engine/scrapers/base.py +386 -0
- fable_engine/scrapers/github.py +129 -0
- fable_engine/scrapers/reddit.py +154 -0
- fable_engine/scrapers/web.py +120 -0
- fable_engine/scrapers/x.py +125 -0
- fable_engine/scrapers/youtube.py +132 -0
- fable_engine/server.py +414 -0
- fable_engine/session.py +1819 -0
- fable_engine/test_server.py +1362 -0
- fable_engine/updater.py +541 -0
- fable_engine-1.3.1.dist-info/LICENSE +22 -0
- fable_engine-1.3.1.dist-info/METADATA +173 -0
- fable_engine-1.3.1.dist-info/RECORD +104 -0
- fable_engine-1.3.1.dist-info/WHEEL +5 -0
- fable_engine-1.3.1.dist-info/entry_points.txt +5 -0
- fable_engine-1.3.1.dist-info/top_level.txt +6 -0
- fable_mode/__init__.py +3 -0
- fable_mode/__main__.py +4 -0
- fable_mode/adapters.py +1014 -0
- fable_mode/installer.py +553 -0
- fable_mode/launcher.py +437 -0
- fable_mode/manifest.py +142 -0
- fable_mode/resources.json +114 -0
- fable_mode/safety.py +103 -0
- fable_mode_entry.py +10 -0
- fable_v2/__init__.py +146 -0
- fable_v2/adapters.py +151 -0
- fable_v2/coder_fleet/__init__.py +100 -0
- fable_v2/coder_fleet/ast_tools.py +158 -0
- fable_v2/coder_fleet/compute.py +199 -0
- fable_v2/coder_fleet/design_engine.py +1316 -0
- fable_v2/coder_fleet/diagnostics.py +293 -0
- fable_v2/coder_fleet/fleet_dispatcher.py +214 -0
- fable_v2/coder_fleet/mock_auditor.py +306 -0
- fable_v2/coder_fleet/mutation.py +216 -0
- fable_v2/coder_fleet/property_oracle.py +260 -0
- fable_v2/coder_fleet/receipt_attestor.py +122 -0
- fable_v2/coder_fleet/red_team_swarm.py +908 -0
- fable_v2/coder_fleet/test_harness.py +198 -0
- fable_v2/coder_fleet/vector_engine.py +1287 -0
- fable_v2/coder_fleet/visual.py +357 -0
- fable_v2/coder_fleet/workspace.py +153 -0
- fable_v2/cortical/__init__.py +20 -0
- fable_v2/cortical/plasticity_engine.py +992 -0
- fable_v2/execution_broker.py +811 -0
- fable_v2/proof_engine.py +1141 -0
- fable_v2/protocol.py +485 -0
- fable_v2/runtime.py +1010 -0
- fable_v2/system3/__init__.py +204 -0
- fable_v2/system3/causal.py +558 -0
- fable_v2/system3/dialectical.py +577 -0
- fable_v2/system3/evolution.py +503 -0
- fable_v2/system3/executive.py +338 -0
- fable_v2/system3/free_energy.py +479 -0
- fable_v2/system3/hyperbolic.py +555 -0
- fable_v2/system3/induction.py +336 -0
- fable_v2/system3/kripke.py +548 -0
- fable_v2/system3/oracle.py +745 -0
- fable_v2/verifiers.py +72 -0
- tests/__init__.py +1 -0
- tests/test_anti_loop_circuit_breaker.py +64 -0
- tests/test_auto_updater.py +407 -0
- tests/test_coder_fleet.py +535 -0
- tests/test_delegation_compiler.py +54 -0
- tests/test_descriptor_boundaries.py +126 -0
- tests/test_design_engine.py +603 -0
- tests/test_epistemic_evidence_validator.py +66 -0
- tests/test_execution_broker.py +233 -0
- tests/test_fable_v2.py +406 -0
- tests/test_fleet_transitions.py +116 -0
- tests/test_fsm_redteam_evolution.py +406 -0
- tests/test_goal_rubric_and_pipeline.py +367 -0
- tests/test_hebbian_plasticity.py +585 -0
- tests/test_packaging_runtime.py +194 -0
- tests/test_proof_engine.py +259 -0
- tests/test_red_team_swarm.py +645 -0
- tests/test_redteam_remediation.py +169 -0
- tests/test_registration_transaction.py +375 -0
- tests/test_requested_regressions.py +467 -0
- tests/test_scrapers.py +370 -0
- tests/test_server_actions.py +93 -0
- tests/test_server_frontier_actions.py +269 -0
- tests/test_server_protocol.py +88 -0
- tests/test_stealth_browser.py +970 -0
- tests/test_system3.py +381 -0
- tests/test_system3_deep_integration.py +385 -0
- tests/test_system3_frontier.py +436 -0
- tests/test_vector_engine.py +608 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Mock Auditor Engine for Detecting Fake Tautological Tests and Mock Leakage.
|
|
2
|
+
|
|
3
|
+
Audits test suites for trivial assertions (assert True, 1==1), excessive mocking,
|
|
4
|
+
and absence of negative / error path tests.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MockAuditorEngine:
|
|
13
|
+
"""Engine for auditing test assertions, tautologies, mock stubs, and negative branch coverage."""
|
|
14
|
+
|
|
15
|
+
def audit_assertions(self, test_code: str) -> dict[str, Any]:
|
|
16
|
+
"""AST visitor detecting trivial assertions (assert True, assert 1 == 1, assert x is not None as lone check),
|
|
17
|
+
|
|
18
|
+
calculating assertion density and flagging tautologies.
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
tree = ast.parse(test_code)
|
|
22
|
+
except SyntaxError:
|
|
23
|
+
return {
|
|
24
|
+
"total_assertions": 0,
|
|
25
|
+
"trivial_assertions_count": 0,
|
|
26
|
+
"tautologies": [],
|
|
27
|
+
"assertion_density": 0.0,
|
|
28
|
+
"passed_audit": False,
|
|
29
|
+
"error": "SyntaxError parsing test code",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
tautologies: list[dict[str, Any]] = []
|
|
33
|
+
total_assertions = 0
|
|
34
|
+
test_functions = 0
|
|
35
|
+
|
|
36
|
+
class AssertionVisitor(ast.NodeVisitor):
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
nonlocal test_functions
|
|
39
|
+
super().__init__()
|
|
40
|
+
|
|
41
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
42
|
+
nonlocal test_functions
|
|
43
|
+
if node.name.startswith("test_") or node.name.endswith("_test"):
|
|
44
|
+
test_functions += 1
|
|
45
|
+
|
|
46
|
+
# Check for lone 'assert x is not None'
|
|
47
|
+
assert_stmts = [s for s in node.body if isinstance(s, ast.Assert)]
|
|
48
|
+
if len(node.body) == 1 and len(assert_stmts) == 1:
|
|
49
|
+
target_assert = assert_stmts[0]
|
|
50
|
+
if isinstance(target_assert.test, ast.Compare):
|
|
51
|
+
ops = target_assert.test.ops
|
|
52
|
+
comparators = target_assert.test.comparators
|
|
53
|
+
if any(isinstance(op, ast.IsNot) for op in ops) and any(
|
|
54
|
+
isinstance(c, ast.Constant) and c.value is None for c in comparators
|
|
55
|
+
):
|
|
56
|
+
tautologies.append({
|
|
57
|
+
"line": target_assert.lineno,
|
|
58
|
+
"col": target_assert.col_offset,
|
|
59
|
+
"type": "lone_not_none_assertion",
|
|
60
|
+
"code_snippet": ast.unparse(target_assert) if hasattr(ast, "unparse") else "assert x is not None",
|
|
61
|
+
"reason": "Test function contains only a single 'assert x is not None' check without behavior verification",
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
self.generic_visit(node)
|
|
65
|
+
|
|
66
|
+
def visit_Assert(self, node: ast.Assert) -> None:
|
|
67
|
+
nonlocal total_assertions
|
|
68
|
+
total_assertions += 1
|
|
69
|
+
|
|
70
|
+
# Check assert True / assert 1
|
|
71
|
+
if isinstance(node.test, ast.Constant):
|
|
72
|
+
if node.test.value is True or node.test.value == 1:
|
|
73
|
+
tautologies.append({
|
|
74
|
+
"line": node.lineno,
|
|
75
|
+
"col": node.col_offset,
|
|
76
|
+
"type": "constant_true_assertion",
|
|
77
|
+
"code_snippet": ast.unparse(node) if hasattr(ast, "unparse") else "assert True",
|
|
78
|
+
"reason": f"Tautological assertion with constant truth value '{node.test.value}'",
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
# Check assert 1 == 1, assert "a" == "a"
|
|
82
|
+
elif isinstance(node.test, ast.Compare):
|
|
83
|
+
cmp = node.test
|
|
84
|
+
if isinstance(cmp.left, ast.Constant) and len(cmp.comparators) == 1 and isinstance(cmp.comparators[0], ast.Constant):
|
|
85
|
+
if cmp.left.value == cmp.comparators[0].value and any(isinstance(op, (ast.Eq, ast.Is)) for op in cmp.ops):
|
|
86
|
+
tautologies.append({
|
|
87
|
+
"line": node.lineno,
|
|
88
|
+
"col": node.col_offset,
|
|
89
|
+
"type": "literal_equality_tautology",
|
|
90
|
+
"code_snippet": ast.unparse(node) if hasattr(ast, "unparse") else "assert a == a",
|
|
91
|
+
"reason": f"Trivial assertion comparing literal {cmp.left.value!r} to itself",
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
self.generic_visit(node)
|
|
95
|
+
|
|
96
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
97
|
+
nonlocal total_assertions
|
|
98
|
+
func_name = ""
|
|
99
|
+
if isinstance(node.func, ast.Attribute):
|
|
100
|
+
func_name = node.func.attr
|
|
101
|
+
elif isinstance(node.func, ast.Name):
|
|
102
|
+
func_name = node.func.id
|
|
103
|
+
|
|
104
|
+
if func_name.startswith("assert"):
|
|
105
|
+
total_assertions += 1
|
|
106
|
+
|
|
107
|
+
if func_name in ("assertTrue", "assert_true") and node.args:
|
|
108
|
+
first_arg = node.args[0]
|
|
109
|
+
if isinstance(first_arg, ast.Constant) and first_arg.value is True:
|
|
110
|
+
tautologies.append({
|
|
111
|
+
"line": node.lineno,
|
|
112
|
+
"col": node.col_offset,
|
|
113
|
+
"type": "assertTrue_true_call",
|
|
114
|
+
"code_snippet": ast.unparse(node) if hasattr(ast, "unparse") else "self.assertTrue(True)",
|
|
115
|
+
"reason": "Trivial assertion call assertTrue(True)",
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
elif func_name in ("assertEqual", "assert_equal") and len(node.args) >= 2:
|
|
119
|
+
a1, a2 = node.args[0], node.args[1]
|
|
120
|
+
if isinstance(a1, ast.Constant) and isinstance(a2, ast.Constant) and a1.value == a2.value:
|
|
121
|
+
tautologies.append({
|
|
122
|
+
"line": node.lineno,
|
|
123
|
+
"col": node.col_offset,
|
|
124
|
+
"type": "assertEqual_identical_call",
|
|
125
|
+
"code_snippet": ast.unparse(node) if hasattr(ast, "unparse") else "self.assertEqual(a, a)",
|
|
126
|
+
"reason": f"Trivial assertion call assertEqual with identical constant {a1.value!r}",
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
self.generic_visit(node)
|
|
130
|
+
|
|
131
|
+
visitor = AssertionVisitor()
|
|
132
|
+
visitor.visit(tree)
|
|
133
|
+
|
|
134
|
+
trivial_count = len(tautologies)
|
|
135
|
+
effective_tests = max(1, test_functions)
|
|
136
|
+
density = round(total_assertions / effective_tests, 2)
|
|
137
|
+
passed = (trivial_count == 0) and (total_assertions > 0)
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
"total_assertions": total_assertions,
|
|
141
|
+
"trivial_assertions_count": trivial_count,
|
|
142
|
+
"tautologies": tautologies,
|
|
143
|
+
"assertion_density": density,
|
|
144
|
+
"passed_audit": passed,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
def detect_mock_leakage(self, test_code: str) -> dict[str, Any]:
|
|
148
|
+
"""Check for excessive mock usage where core logic is stubbed out."""
|
|
149
|
+
try:
|
|
150
|
+
tree = ast.parse(test_code)
|
|
151
|
+
except SyntaxError:
|
|
152
|
+
return {
|
|
153
|
+
"mock_count": 0,
|
|
154
|
+
"mock_ratio": 0.0,
|
|
155
|
+
"flagged_mocks": [],
|
|
156
|
+
"has_excessive_mocking": False,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
mock_identifiers = {"Mock", "MagicMock", "patch", "mocker", "monkeypatch", "PropertyMock"}
|
|
160
|
+
mock_count = 0
|
|
161
|
+
total_calls = 0
|
|
162
|
+
flagged: list[dict[str, Any]] = []
|
|
163
|
+
|
|
164
|
+
class MockVisitor(ast.NodeVisitor):
|
|
165
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
166
|
+
nonlocal mock_count, total_calls
|
|
167
|
+
total_calls += 1
|
|
168
|
+
name = ""
|
|
169
|
+
if isinstance(node.func, ast.Name):
|
|
170
|
+
name = node.func.id
|
|
171
|
+
elif isinstance(node.func, ast.Attribute):
|
|
172
|
+
name = node.func.attr
|
|
173
|
+
|
|
174
|
+
if name in mock_identifiers or "mock" in name.lower():
|
|
175
|
+
mock_count += 1
|
|
176
|
+
flagged.append({
|
|
177
|
+
"line": node.lineno,
|
|
178
|
+
"name": name,
|
|
179
|
+
"reason": f"Invocation of mock factory or stub '{name}'",
|
|
180
|
+
})
|
|
181
|
+
self.generic_visit(node)
|
|
182
|
+
|
|
183
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
184
|
+
nonlocal mock_count
|
|
185
|
+
for dec in node.decorator_list:
|
|
186
|
+
dec_name = ""
|
|
187
|
+
if isinstance(dec, ast.Name):
|
|
188
|
+
dec_name = dec.id
|
|
189
|
+
elif isinstance(dec, ast.Attribute):
|
|
190
|
+
dec_name = dec.attr
|
|
191
|
+
elif isinstance(dec, ast.Call):
|
|
192
|
+
if isinstance(dec.func, ast.Name):
|
|
193
|
+
dec_name = dec.func.id
|
|
194
|
+
elif isinstance(dec.func, ast.Attribute):
|
|
195
|
+
dec_name = dec.func.attr
|
|
196
|
+
|
|
197
|
+
if "patch" in dec_name.lower():
|
|
198
|
+
mock_count += 1
|
|
199
|
+
flagged.append({
|
|
200
|
+
"line": dec.lineno if hasattr(dec, "lineno") else node.lineno,
|
|
201
|
+
"name": dec_name,
|
|
202
|
+
"reason": f"Patch decorator '@{dec_name}' stubbing dependencies",
|
|
203
|
+
})
|
|
204
|
+
self.generic_visit(node)
|
|
205
|
+
|
|
206
|
+
MockVisitor().visit(tree)
|
|
207
|
+
ratio = round(mock_count / max(1, total_calls), 2)
|
|
208
|
+
excessive = ratio > 0.5 or (mock_count >= 5 and ratio > 0.3)
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
"mock_count": mock_count,
|
|
212
|
+
"mock_ratio": ratio,
|
|
213
|
+
"flagged_mocks": flagged,
|
|
214
|
+
"has_excessive_mocking": excessive,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
def enforce_negative_paths(self, test_code: str) -> dict[str, Any]:
|
|
218
|
+
"""Verify presence of negative test cases (assertRaises, except, pytest.raises)."""
|
|
219
|
+
try:
|
|
220
|
+
tree = ast.parse(test_code)
|
|
221
|
+
except SyntaxError:
|
|
222
|
+
return {
|
|
223
|
+
"has_negative_tests": False,
|
|
224
|
+
"negative_test_count": 0,
|
|
225
|
+
"total_test_cases": 0,
|
|
226
|
+
"negative_ratio": 0.0,
|
|
227
|
+
"details": [],
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
negative_details: list[dict[str, Any]] = []
|
|
231
|
+
total_test_cases = 0
|
|
232
|
+
|
|
233
|
+
class NegativePathVisitor(ast.NodeVisitor):
|
|
234
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
235
|
+
nonlocal total_test_cases
|
|
236
|
+
if node.name.startswith("test_") or node.name.endswith("_test"):
|
|
237
|
+
total_test_cases += 1
|
|
238
|
+
# Check if function name indicates negative test
|
|
239
|
+
lower_name = node.name.lower()
|
|
240
|
+
if any(w in lower_name for w in ("error", "invalid", "fail", "exception", "reject", "negative")):
|
|
241
|
+
negative_details.append({
|
|
242
|
+
"line": node.lineno,
|
|
243
|
+
"function": node.name,
|
|
244
|
+
"type": "naming_convention",
|
|
245
|
+
"description": f"Negative test detected from name '{node.name}'",
|
|
246
|
+
})
|
|
247
|
+
self.generic_visit(node)
|
|
248
|
+
|
|
249
|
+
def visit_With(self, node: ast.With) -> None:
|
|
250
|
+
for item in node.items:
|
|
251
|
+
expr = item.context_expr
|
|
252
|
+
expr_name = ""
|
|
253
|
+
if isinstance(expr, ast.Call):
|
|
254
|
+
if isinstance(expr.func, ast.Attribute):
|
|
255
|
+
expr_name = expr.func.attr
|
|
256
|
+
elif isinstance(expr.func, ast.Name):
|
|
257
|
+
expr_name = expr.func.id
|
|
258
|
+
|
|
259
|
+
if expr_name in ("assertRaises", "raises", "assert_raises"):
|
|
260
|
+
negative_details.append({
|
|
261
|
+
"line": node.lineno,
|
|
262
|
+
"function": None,
|
|
263
|
+
"type": "with_assertRaises",
|
|
264
|
+
"description": f"Context manager '{expr_name}' testing error expectation",
|
|
265
|
+
})
|
|
266
|
+
self.generic_visit(node)
|
|
267
|
+
|
|
268
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
269
|
+
func_name = ""
|
|
270
|
+
if isinstance(node.func, ast.Attribute):
|
|
271
|
+
func_name = node.func.attr
|
|
272
|
+
elif isinstance(node.func, ast.Name):
|
|
273
|
+
func_name = node.func.id
|
|
274
|
+
|
|
275
|
+
if func_name in ("assertRaises", "assert_raises", "assertFalse"):
|
|
276
|
+
negative_details.append({
|
|
277
|
+
"line": node.lineno,
|
|
278
|
+
"function": None,
|
|
279
|
+
"type": "negative_assert_call",
|
|
280
|
+
"description": f"Assertion call '{func_name}' testing negative/failure path",
|
|
281
|
+
})
|
|
282
|
+
self.generic_visit(node)
|
|
283
|
+
|
|
284
|
+
def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
|
|
285
|
+
# Presence of an except handler inside a test
|
|
286
|
+
negative_details.append({
|
|
287
|
+
"line": node.lineno,
|
|
288
|
+
"function": None,
|
|
289
|
+
"type": "try_except_block",
|
|
290
|
+
"description": "Explicit exception handling checking error recovery",
|
|
291
|
+
})
|
|
292
|
+
self.generic_visit(node)
|
|
293
|
+
|
|
294
|
+
NegativePathVisitor().visit(tree)
|
|
295
|
+
|
|
296
|
+
neg_count = len(negative_details)
|
|
297
|
+
effective_total = max(1, total_test_cases)
|
|
298
|
+
ratio = round(neg_count / effective_total, 2)
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
"has_negative_tests": neg_count > 0,
|
|
302
|
+
"negative_test_count": neg_count,
|
|
303
|
+
"total_test_cases": total_test_cases,
|
|
304
|
+
"negative_ratio": ratio,
|
|
305
|
+
"details": negative_details,
|
|
306
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Mutation Verifier Engine for AST-based Mutant Injection and Test Rigor Auditing.
|
|
2
|
+
|
|
3
|
+
Generates semantic AST mutations and verifies whether test suites actually catch mutants
|
|
4
|
+
or whether tests are fake / tautological.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
import copy
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .test_harness import TestHarnessEngine
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MutationVerifierEngine:
|
|
16
|
+
"""Engine for generating AST-based code mutants and assessing test suite kill rates."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, test_harness: TestHarnessEngine | None = None) -> None:
|
|
19
|
+
self._harness = test_harness or TestHarnessEngine()
|
|
20
|
+
|
|
21
|
+
def inject_mutants(self, source_code: str) -> list[dict[str, Any]]:
|
|
22
|
+
"""Generate 3-5 AST-based mutants:
|
|
23
|
+
|
|
24
|
+
- inverts >/<, ==/!=
|
|
25
|
+
- flips True/False
|
|
26
|
+
- replaces numeric constants
|
|
27
|
+
- removes return statements / alters return values
|
|
28
|
+
- inverts arithmetic operators (+/-)
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
tree = ast.parse(source_code)
|
|
32
|
+
except SyntaxError:
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
# Find mutation candidates
|
|
36
|
+
candidates: list[tuple[ast.AST, str, Any]] = []
|
|
37
|
+
|
|
38
|
+
for node in ast.walk(tree):
|
|
39
|
+
# 1. Comparison operations
|
|
40
|
+
if isinstance(node, ast.Compare):
|
|
41
|
+
for op in node.ops:
|
|
42
|
+
if isinstance(op, ast.Gt):
|
|
43
|
+
candidates.append((node, "invert_comparison_gt_to_lt", (op, ast.Lt())))
|
|
44
|
+
elif isinstance(op, ast.Lt):
|
|
45
|
+
candidates.append((node, "invert_comparison_lt_to_gt", (op, ast.Gt())))
|
|
46
|
+
elif isinstance(op, ast.GtE):
|
|
47
|
+
candidates.append((node, "invert_comparison_gte_to_lte", (op, ast.LtE())))
|
|
48
|
+
elif isinstance(op, ast.LtE):
|
|
49
|
+
candidates.append((node, "invert_comparison_lte_to_gte", (op, ast.GtE())))
|
|
50
|
+
elif isinstance(op, ast.Eq):
|
|
51
|
+
candidates.append((node, "invert_comparison_eq_to_neq", (op, ast.NotEq())))
|
|
52
|
+
elif isinstance(op, ast.NotEq):
|
|
53
|
+
candidates.append((node, "invert_comparison_neq_to_eq", (op, ast.Eq())))
|
|
54
|
+
|
|
55
|
+
# 2. Boolean and numeric constants
|
|
56
|
+
elif isinstance(node, ast.Constant):
|
|
57
|
+
if node.value is True:
|
|
58
|
+
candidates.append((node, "flip_boolean_true_to_false", False))
|
|
59
|
+
elif node.value is False:
|
|
60
|
+
candidates.append((node, "flip_boolean_false_to_true", True))
|
|
61
|
+
elif isinstance(node.value, (int, float)) and not isinstance(node.value, bool):
|
|
62
|
+
new_val = 1 if node.value == 0 else (node.value + 1)
|
|
63
|
+
candidates.append((node, "mutate_numeric_constant", new_val))
|
|
64
|
+
|
|
65
|
+
# 3. Return statements
|
|
66
|
+
elif isinstance(node, ast.Return):
|
|
67
|
+
if node.value is not None:
|
|
68
|
+
candidates.append((node, "remove_return_value", None))
|
|
69
|
+
|
|
70
|
+
# 4. Arithmetic operators
|
|
71
|
+
elif isinstance(node, ast.BinOp):
|
|
72
|
+
if isinstance(node.op, ast.Add):
|
|
73
|
+
candidates.append((node, "invert_arithmetic_add_to_sub", ast.Sub()))
|
|
74
|
+
elif isinstance(node.op, ast.Sub):
|
|
75
|
+
candidates.append((node, "invert_arithmetic_sub_to_add", ast.Add()))
|
|
76
|
+
|
|
77
|
+
mutants: list[dict[str, Any]] = []
|
|
78
|
+
seen_codes: set[str] = {source_code.strip()}
|
|
79
|
+
|
|
80
|
+
for target_node, mut_type, mut_arg in candidates:
|
|
81
|
+
if len(mutants) >= 5:
|
|
82
|
+
break
|
|
83
|
+
|
|
84
|
+
mutated_tree = copy.deepcopy(tree)
|
|
85
|
+
|
|
86
|
+
class Mutator(ast.NodeTransformer):
|
|
87
|
+
def __init__(self, target_line: int, target_col: int) -> None:
|
|
88
|
+
self.target_line = target_line
|
|
89
|
+
self.target_col = target_col
|
|
90
|
+
self.mutated = False
|
|
91
|
+
|
|
92
|
+
def visit_Compare(self, n: ast.Compare) -> ast.AST:
|
|
93
|
+
self.generic_visit(n)
|
|
94
|
+
if not self.mutated and getattr(n, "lineno", -1) == self.target_line and getattr(n, "col_offset", -1) == self.target_col:
|
|
95
|
+
if mut_type.startswith("invert_comparison"):
|
|
96
|
+
old_op, new_op = mut_arg
|
|
97
|
+
new_ops = []
|
|
98
|
+
for op in n.ops:
|
|
99
|
+
if type(op) is type(old_op) and not self.mutated:
|
|
100
|
+
new_ops.append(new_op)
|
|
101
|
+
self.mutated = True
|
|
102
|
+
else:
|
|
103
|
+
new_ops.append(op)
|
|
104
|
+
n.ops = new_ops
|
|
105
|
+
return n
|
|
106
|
+
|
|
107
|
+
def visit_Constant(self, n: ast.Constant) -> ast.AST:
|
|
108
|
+
if not self.mutated and getattr(n, "lineno", -1) == self.target_line and getattr(n, "col_offset", -1) == self.target_col:
|
|
109
|
+
if mut_type in ("flip_boolean_true_to_false", "flip_boolean_false_to_true", "mutate_numeric_constant"):
|
|
110
|
+
n.value = mut_arg
|
|
111
|
+
self.mutated = True
|
|
112
|
+
return n
|
|
113
|
+
|
|
114
|
+
def visit_Return(self, n: ast.Return) -> ast.AST:
|
|
115
|
+
self.generic_visit(n)
|
|
116
|
+
if not self.mutated and getattr(n, "lineno", -1) == self.target_line and getattr(n, "col_offset", -1) == self.target_col:
|
|
117
|
+
if mut_type == "remove_return_value":
|
|
118
|
+
n.value = ast.Constant(value=None)
|
|
119
|
+
self.mutated = True
|
|
120
|
+
return n
|
|
121
|
+
|
|
122
|
+
def visit_BinOp(self, n: ast.BinOp) -> ast.AST:
|
|
123
|
+
self.generic_visit(n)
|
|
124
|
+
if not self.mutated and getattr(n, "lineno", -1) == self.target_line and getattr(n, "col_offset", -1) == self.target_col:
|
|
125
|
+
if mut_type.startswith("invert_arithmetic"):
|
|
126
|
+
n.op = mut_arg
|
|
127
|
+
self.mutated = True
|
|
128
|
+
return n
|
|
129
|
+
|
|
130
|
+
line = getattr(target_node, "lineno", 1)
|
|
131
|
+
col = getattr(target_node, "col_offset", 0)
|
|
132
|
+
mutator = Mutator(line, col)
|
|
133
|
+
mutated_tree = mutator.visit(mutated_tree)
|
|
134
|
+
ast.fix_missing_locations(mutated_tree)
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
mutated_code = ast.unparse(mutated_tree)
|
|
138
|
+
except Exception:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
if mutated_code.strip() not in seen_codes:
|
|
142
|
+
seen_codes.add(mutated_code.strip())
|
|
143
|
+
mutants.append({
|
|
144
|
+
"mutant_id": f"mutant_{len(mutants) + 1}",
|
|
145
|
+
"mutated_code": mutated_code,
|
|
146
|
+
"mutation_type": mut_type,
|
|
147
|
+
"line": line,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
return mutants
|
|
151
|
+
|
|
152
|
+
def audit_test_strength(self, source_code: str, test_code: str, timeout_sec: float = 3.0) -> dict[str, Any]:
|
|
153
|
+
"""Run test_code against each mutant using run_scratch_test.
|
|
154
|
+
|
|
155
|
+
If tests still pass against mutant, mutant survived (test is weak/fake!).
|
|
156
|
+
If tests fail, mutant killed.
|
|
157
|
+
Returns {mutants_total, mutants_killed, mutants_survived, mutation_score, is_thorough}.
|
|
158
|
+
"""
|
|
159
|
+
baseline_code = f"{source_code}\n\n{test_code}"
|
|
160
|
+
baseline_run = self._harness.run_scratch_test(baseline_code, timeout_sec=timeout_sec)
|
|
161
|
+
|
|
162
|
+
if not baseline_run["success"]:
|
|
163
|
+
return {
|
|
164
|
+
"mutants_total": 0,
|
|
165
|
+
"mutants_killed": 0,
|
|
166
|
+
"mutants_survived": 0,
|
|
167
|
+
"mutation_score": 0.0,
|
|
168
|
+
"is_thorough": False,
|
|
169
|
+
"error": f"Baseline test failed on unmutated code: {baseline_run.get('stderr')}",
|
|
170
|
+
"survived_mutant_details": [],
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
mutants = self.inject_mutants(source_code)
|
|
174
|
+
if not mutants:
|
|
175
|
+
return {
|
|
176
|
+
"mutants_total": 0,
|
|
177
|
+
"mutants_killed": 0,
|
|
178
|
+
"mutants_survived": 0,
|
|
179
|
+
"mutation_score": 1.0,
|
|
180
|
+
"is_thorough": True,
|
|
181
|
+
"survived_mutant_details": [],
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
killed: list[dict[str, Any]] = []
|
|
185
|
+
survived: list[dict[str, Any]] = []
|
|
186
|
+
|
|
187
|
+
for m in mutants:
|
|
188
|
+
mutant_test = f"{m['mutated_code']}\n\n{test_code}"
|
|
189
|
+
run_res = self._harness.run_scratch_test(mutant_test, timeout_sec=timeout_sec)
|
|
190
|
+
|
|
191
|
+
if not run_res["success"]:
|
|
192
|
+
# Mutant killed! The test failed as expected on mutated code.
|
|
193
|
+
killed.append(m)
|
|
194
|
+
else:
|
|
195
|
+
# Mutant survived! The test erroneously passed despite bug injected.
|
|
196
|
+
survived.append({
|
|
197
|
+
"mutant_id": m["mutant_id"],
|
|
198
|
+
"mutation_type": m["mutation_type"],
|
|
199
|
+
"line": m["line"],
|
|
200
|
+
"reason": "Test suite passed despite mutation (weak or missing assertion)",
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
total = len(mutants)
|
|
204
|
+
killed_count = len(killed)
|
|
205
|
+
survived_count = len(survived)
|
|
206
|
+
score = round(killed_count / total, 4) if total > 0 else 1.0
|
|
207
|
+
is_thorough = score >= 0.75
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
"mutants_total": total,
|
|
211
|
+
"mutants_killed": killed_count,
|
|
212
|
+
"mutants_survived": survived_count,
|
|
213
|
+
"mutation_score": score,
|
|
214
|
+
"is_thorough": is_thorough,
|
|
215
|
+
"survived_mutant_details": survived,
|
|
216
|
+
}
|