crosshair-tool 0.0.99__cp312-cp312-macosx_10_13_x86_64.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.
- _crosshair_tracers.cpython-312-darwin.so +0 -0
- crosshair/__init__.py +42 -0
- crosshair/__main__.py +8 -0
- crosshair/_mark_stacks.h +790 -0
- crosshair/_preliminaries_test.py +18 -0
- crosshair/_tracers.h +94 -0
- crosshair/_tracers_pycompat.h +522 -0
- crosshair/_tracers_test.py +138 -0
- crosshair/abcstring.py +245 -0
- crosshair/auditwall.py +190 -0
- crosshair/auditwall_test.py +77 -0
- crosshair/codeconfig.py +113 -0
- crosshair/codeconfig_test.py +117 -0
- crosshair/condition_parser.py +1237 -0
- crosshair/condition_parser_test.py +497 -0
- crosshair/conftest.py +30 -0
- crosshair/copyext.py +155 -0
- crosshair/copyext_test.py +84 -0
- crosshair/core.py +1763 -0
- crosshair/core_and_libs.py +149 -0
- crosshair/core_regestered_types_test.py +82 -0
- crosshair/core_test.py +1316 -0
- crosshair/diff_behavior.py +314 -0
- crosshair/diff_behavior_test.py +261 -0
- crosshair/dynamic_typing.py +346 -0
- crosshair/dynamic_typing_test.py +210 -0
- crosshair/enforce.py +282 -0
- crosshair/enforce_test.py +182 -0
- crosshair/examples/PEP316/__init__.py +1 -0
- crosshair/examples/PEP316/bugs_detected/__init__.py +0 -0
- crosshair/examples/PEP316/bugs_detected/getattr_magic.py +16 -0
- crosshair/examples/PEP316/bugs_detected/hash_consistent_with_equals.py +31 -0
- crosshair/examples/PEP316/bugs_detected/shopping_cart.py +24 -0
- crosshair/examples/PEP316/bugs_detected/showcase.py +39 -0
- crosshair/examples/PEP316/correct_code/__init__.py +0 -0
- crosshair/examples/PEP316/correct_code/arith.py +60 -0
- crosshair/examples/PEP316/correct_code/chess.py +77 -0
- crosshair/examples/PEP316/correct_code/nesting_inference.py +17 -0
- crosshair/examples/PEP316/correct_code/numpy_examples.py +132 -0
- crosshair/examples/PEP316/correct_code/rolling_average.py +35 -0
- crosshair/examples/PEP316/correct_code/showcase.py +104 -0
- crosshair/examples/__init__.py +0 -0
- crosshair/examples/check_examples_test.py +146 -0
- crosshair/examples/deal/__init__.py +1 -0
- crosshair/examples/icontract/__init__.py +1 -0
- crosshair/examples/icontract/bugs_detected/__init__.py +0 -0
- crosshair/examples/icontract/bugs_detected/showcase.py +41 -0
- crosshair/examples/icontract/bugs_detected/wrong_sign.py +8 -0
- crosshair/examples/icontract/correct_code/__init__.py +0 -0
- crosshair/examples/icontract/correct_code/arith.py +51 -0
- crosshair/examples/icontract/correct_code/showcase.py +94 -0
- crosshair/fnutil.py +391 -0
- crosshair/fnutil_test.py +75 -0
- crosshair/fuzz_core_test.py +516 -0
- crosshair/libimpl/__init__.py +0 -0
- crosshair/libimpl/arraylib.py +161 -0
- crosshair/libimpl/binascii_ch_test.py +30 -0
- crosshair/libimpl/binascii_test.py +67 -0
- crosshair/libimpl/binasciilib.py +150 -0
- crosshair/libimpl/bisectlib_test.py +23 -0
- crosshair/libimpl/builtinslib.py +5228 -0
- crosshair/libimpl/builtinslib_ch_test.py +1191 -0
- crosshair/libimpl/builtinslib_test.py +3735 -0
- crosshair/libimpl/codecslib.py +86 -0
- crosshair/libimpl/codecslib_test.py +86 -0
- crosshair/libimpl/collectionslib.py +264 -0
- crosshair/libimpl/collectionslib_ch_test.py +252 -0
- crosshair/libimpl/collectionslib_test.py +332 -0
- crosshair/libimpl/copylib.py +23 -0
- crosshair/libimpl/copylib_test.py +18 -0
- crosshair/libimpl/datetimelib.py +2559 -0
- crosshair/libimpl/datetimelib_ch_test.py +354 -0
- crosshair/libimpl/datetimelib_test.py +112 -0
- crosshair/libimpl/decimallib.py +5257 -0
- crosshair/libimpl/decimallib_ch_test.py +78 -0
- crosshair/libimpl/decimallib_test.py +76 -0
- crosshair/libimpl/encodings/__init__.py +23 -0
- crosshair/libimpl/encodings/_encutil.py +187 -0
- crosshair/libimpl/encodings/ascii.py +44 -0
- crosshair/libimpl/encodings/latin_1.py +40 -0
- crosshair/libimpl/encodings/utf_8.py +93 -0
- crosshair/libimpl/encodings_ch_test.py +83 -0
- crosshair/libimpl/fractionlib.py +16 -0
- crosshair/libimpl/fractionlib_test.py +80 -0
- crosshair/libimpl/functoolslib.py +34 -0
- crosshair/libimpl/functoolslib_test.py +56 -0
- crosshair/libimpl/hashliblib.py +30 -0
- crosshair/libimpl/hashliblib_test.py +18 -0
- crosshair/libimpl/heapqlib.py +47 -0
- crosshair/libimpl/heapqlib_test.py +21 -0
- crosshair/libimpl/importliblib.py +18 -0
- crosshair/libimpl/importliblib_test.py +38 -0
- crosshair/libimpl/iolib.py +216 -0
- crosshair/libimpl/iolib_ch_test.py +128 -0
- crosshair/libimpl/iolib_test.py +19 -0
- crosshair/libimpl/ipaddresslib.py +8 -0
- crosshair/libimpl/itertoolslib.py +44 -0
- crosshair/libimpl/itertoolslib_test.py +44 -0
- crosshair/libimpl/jsonlib.py +984 -0
- crosshair/libimpl/jsonlib_ch_test.py +42 -0
- crosshair/libimpl/jsonlib_test.py +51 -0
- crosshair/libimpl/mathlib.py +179 -0
- crosshair/libimpl/mathlib_ch_test.py +44 -0
- crosshair/libimpl/mathlib_test.py +67 -0
- crosshair/libimpl/oslib.py +7 -0
- crosshair/libimpl/pathliblib_test.py +10 -0
- crosshair/libimpl/randomlib.py +178 -0
- crosshair/libimpl/randomlib_test.py +120 -0
- crosshair/libimpl/relib.py +846 -0
- crosshair/libimpl/relib_ch_test.py +169 -0
- crosshair/libimpl/relib_test.py +493 -0
- crosshair/libimpl/timelib.py +72 -0
- crosshair/libimpl/timelib_test.py +82 -0
- crosshair/libimpl/typeslib.py +15 -0
- crosshair/libimpl/typeslib_test.py +36 -0
- crosshair/libimpl/unicodedatalib.py +75 -0
- crosshair/libimpl/unicodedatalib_test.py +42 -0
- crosshair/libimpl/urlliblib.py +23 -0
- crosshair/libimpl/urlliblib_test.py +19 -0
- crosshair/libimpl/weakreflib.py +13 -0
- crosshair/libimpl/weakreflib_test.py +69 -0
- crosshair/libimpl/zliblib.py +15 -0
- crosshair/libimpl/zliblib_test.py +13 -0
- crosshair/lsp_server.py +261 -0
- crosshair/lsp_server_test.py +30 -0
- crosshair/main.py +973 -0
- crosshair/main_test.py +543 -0
- crosshair/objectproxy.py +376 -0
- crosshair/objectproxy_test.py +41 -0
- crosshair/opcode_intercept.py +601 -0
- crosshair/opcode_intercept_test.py +304 -0
- crosshair/options.py +218 -0
- crosshair/options_test.py +10 -0
- crosshair/patch_equivalence_test.py +75 -0
- crosshair/path_cover.py +209 -0
- crosshair/path_cover_test.py +138 -0
- crosshair/path_search.py +161 -0
- crosshair/path_search_test.py +52 -0
- crosshair/pathing_oracle.py +271 -0
- crosshair/pathing_oracle_test.py +21 -0
- crosshair/pure_importer.py +27 -0
- crosshair/pure_importer_test.py +16 -0
- crosshair/py.typed +0 -0
- crosshair/register_contract.py +273 -0
- crosshair/register_contract_test.py +190 -0
- crosshair/simplestructs.py +1165 -0
- crosshair/simplestructs_test.py +283 -0
- crosshair/smtlib.py +24 -0
- crosshair/smtlib_test.py +14 -0
- crosshair/statespace.py +1199 -0
- crosshair/statespace_test.py +108 -0
- crosshair/stubs_parser.py +352 -0
- crosshair/stubs_parser_test.py +43 -0
- crosshair/test_util.py +329 -0
- crosshair/test_util_test.py +26 -0
- crosshair/tools/__init__.py +0 -0
- crosshair/tools/check_help_in_doc.py +264 -0
- crosshair/tools/check_init_and_setup_coincide.py +119 -0
- crosshair/tools/generate_demo_table.py +127 -0
- crosshair/tracers.py +544 -0
- crosshair/tracers_test.py +154 -0
- crosshair/type_repo.py +151 -0
- crosshair/unicode_categories.py +589 -0
- crosshair/unicode_categories_test.py +27 -0
- crosshair/util.py +741 -0
- crosshair/util_test.py +173 -0
- crosshair/watcher.py +307 -0
- crosshair/watcher_test.py +107 -0
- crosshair/z3util.py +76 -0
- crosshair/z3util_test.py +11 -0
- crosshair_tool-0.0.99.dist-info/METADATA +144 -0
- crosshair_tool-0.0.99.dist-info/RECORD +176 -0
- crosshair_tool-0.0.99.dist-info/WHEEL +6 -0
- crosshair_tool-0.0.99.dist-info/entry_points.txt +3 -0
- crosshair_tool-0.0.99.dist-info/licenses/LICENSE +93 -0
- crosshair_tool-0.0.99.dist-info/top_level.txt +2 -0
crosshair/tracers.py
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
"""Provide access to and overrides for functions as they are called."""
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import dataclasses
|
|
5
|
+
import dis
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import types
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from sys import _getframe
|
|
11
|
+
from types import CodeType
|
|
12
|
+
from typing import (
|
|
13
|
+
Any,
|
|
14
|
+
Callable,
|
|
15
|
+
DefaultDict,
|
|
16
|
+
Dict,
|
|
17
|
+
FrozenSet,
|
|
18
|
+
Iterable,
|
|
19
|
+
List,
|
|
20
|
+
Optional,
|
|
21
|
+
Set,
|
|
22
|
+
Tuple,
|
|
23
|
+
TypeVar,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from _crosshair_tracers import CTracer, TraceSwap, supported_opcodes # type: ignore
|
|
27
|
+
|
|
28
|
+
CROSSHAIR_EXTRA_ASSERTS = os.environ.get("CROSSHAIR_EXTRA_ASSERTS", "0") == "1"
|
|
29
|
+
|
|
30
|
+
SYS_MONITORING_TOOL_ID = 4
|
|
31
|
+
USE_C_TRACER = True
|
|
32
|
+
|
|
33
|
+
PyObjPtr = ctypes.POINTER(ctypes.py_object)
|
|
34
|
+
Py_IncRef = ctypes.pythonapi.Py_IncRef
|
|
35
|
+
Py_DecRef = ctypes.pythonapi.Py_DecRef
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_debug_header: Tuple[Tuple[str, type], ...] = (
|
|
39
|
+
(
|
|
40
|
+
("_ob_next", PyObjPtr),
|
|
41
|
+
("_ob_prev", PyObjPtr),
|
|
42
|
+
)
|
|
43
|
+
if sys.flags.debug
|
|
44
|
+
else ()
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
from _crosshair_tracers import frame_stack_read, frame_stack_write
|
|
49
|
+
|
|
50
|
+
CALL_FUNCTION = dis.opmap.get("CALL_FUNCTION", 256)
|
|
51
|
+
CALL_FUNCTION_KW = dis.opmap.get("CALL_FUNCTION_KW", 256) # Removed as of 3.11
|
|
52
|
+
CALL_FUNCTION_EX = dis.opmap.get("CALL_FUNCTION_EX", 256)
|
|
53
|
+
CALL_METHOD = dis.opmap.get("CALL_METHOD", 256)
|
|
54
|
+
BUILD_TUPLE_UNPACK_WITH_CALL = dis.opmap.get("BUILD_TUPLE_UNPACK_WITH_CALL", 256)
|
|
55
|
+
CALL = dis.opmap.get("CALL", 256)
|
|
56
|
+
CALL_KW = dis.opmap.get("CALL_KW", 256) # New in 3.13
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RawNullPointer:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
NULL_POINTER = RawNullPointer()
|
|
64
|
+
CallStackInfo = (
|
|
65
|
+
Tuple[ # Information about the interpreter stack just before calling a function
|
|
66
|
+
int, # stack index of the callable
|
|
67
|
+
Callable, # the callable object itself
|
|
68
|
+
Optional[int], # index of kwargs dict (if used in this call)
|
|
69
|
+
]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def handle_build_tuple_unpack_with_call(frame) -> CallStackInfo:
|
|
74
|
+
idx = -(
|
|
75
|
+
frame.f_code.co_code[frame.f_lasti + 1] + 1
|
|
76
|
+
) # TODO: account for EXTENDED_ARG, here and elsewhere
|
|
77
|
+
try:
|
|
78
|
+
return (idx, frame_stack_read(frame, idx), None)
|
|
79
|
+
except ValueError:
|
|
80
|
+
return (idx, NULL_POINTER) # type: ignore
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def handle_call_3_11(frame) -> CallStackInfo:
|
|
84
|
+
idx = -(frame.f_code.co_code[frame.f_lasti + 1] + 1)
|
|
85
|
+
try:
|
|
86
|
+
ret = (idx - 1, frame_stack_read(frame, idx - 1), None)
|
|
87
|
+
except ValueError:
|
|
88
|
+
ret = (idx, frame_stack_read(frame, idx), None)
|
|
89
|
+
return ret
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def handle_call_3_13(frame) -> CallStackInfo:
|
|
93
|
+
idx = -(frame.f_code.co_code[frame.f_lasti + 1] + 2)
|
|
94
|
+
return (idx, frame_stack_read(frame, idx), None)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def handle_call_function(frame) -> CallStackInfo:
|
|
98
|
+
idx = -(frame.f_code.co_code[frame.f_lasti + 1] + 1)
|
|
99
|
+
try:
|
|
100
|
+
return (idx, frame_stack_read(frame, idx), None)
|
|
101
|
+
except ValueError:
|
|
102
|
+
return (idx, NULL_POINTER) # type: ignore
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def handle_call_function_kw(frame) -> CallStackInfo:
|
|
106
|
+
idx = -(frame.f_code.co_code[frame.f_lasti + 1] + 2)
|
|
107
|
+
try:
|
|
108
|
+
return (idx, frame_stack_read(frame, idx), None)
|
|
109
|
+
except ValueError:
|
|
110
|
+
return (idx, NULL_POINTER) # type: ignore
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def handle_call_kw(frame) -> CallStackInfo:
|
|
114
|
+
idx = -(frame.f_code.co_code[frame.f_lasti + 1] + 3)
|
|
115
|
+
return (idx, frame_stack_read(frame, idx), None)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def handle_call_function_ex_3_6(frame) -> CallStackInfo:
|
|
119
|
+
has_kwargs = frame.f_code.co_code[frame.f_lasti + 1] & 1
|
|
120
|
+
idx = -(has_kwargs + 2)
|
|
121
|
+
kwargs_idx = -1 if has_kwargs else None
|
|
122
|
+
try:
|
|
123
|
+
return (idx, frame_stack_read(frame, idx), kwargs_idx)
|
|
124
|
+
except ValueError:
|
|
125
|
+
return (idx, NULL_POINTER, kwargs_idx) # type: ignore
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def handle_call_function_ex_3_13(frame) -> CallStackInfo:
|
|
129
|
+
has_kwargs = frame.f_code.co_code[frame.f_lasti + 1] & 1
|
|
130
|
+
idx = -(has_kwargs + 3)
|
|
131
|
+
kwargs_idx = -1 if has_kwargs else None
|
|
132
|
+
try:
|
|
133
|
+
return (idx, frame_stack_read(frame, idx), kwargs_idx)
|
|
134
|
+
except ValueError:
|
|
135
|
+
return (idx, NULL_POINTER, kwargs_idx) # type: ignore
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def handle_call_function_ex_3_14(frame) -> CallStackInfo:
|
|
139
|
+
callable_idx, kwargs_idx = -4, -1
|
|
140
|
+
try:
|
|
141
|
+
return (callable_idx, frame_stack_read(frame, callable_idx), kwargs_idx)
|
|
142
|
+
except ValueError:
|
|
143
|
+
return (callable_idx, NULL_POINTER, kwargs_idx) # type: ignore
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def handle_call_method(frame) -> CallStackInfo:
|
|
147
|
+
idx = -(frame.f_code.co_code[frame.f_lasti + 1] + 2)
|
|
148
|
+
try:
|
|
149
|
+
return (idx, frame_stack_read(frame, idx), None)
|
|
150
|
+
except ValueError:
|
|
151
|
+
# not a sucessful method lookup; no call happens here
|
|
152
|
+
idx += 1
|
|
153
|
+
return (idx, frame_stack_read(frame, idx), None)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
_CALL_HANDLERS: Dict[int, Callable[[object], CallStackInfo]] = {
|
|
157
|
+
BUILD_TUPLE_UNPACK_WITH_CALL: handle_build_tuple_unpack_with_call,
|
|
158
|
+
CALL: handle_call_3_13 if sys.version_info >= (3, 13) else handle_call_3_11,
|
|
159
|
+
CALL_KW: handle_call_kw,
|
|
160
|
+
CALL_FUNCTION: handle_call_function,
|
|
161
|
+
CALL_FUNCTION_KW: handle_call_function_kw,
|
|
162
|
+
CALL_FUNCTION_EX: (
|
|
163
|
+
handle_call_function_ex_3_14
|
|
164
|
+
if sys.version_info >= (3, 14)
|
|
165
|
+
else (
|
|
166
|
+
handle_call_function_ex_3_13
|
|
167
|
+
if sys.version_info >= (3, 13)
|
|
168
|
+
else handle_call_function_ex_3_6
|
|
169
|
+
)
|
|
170
|
+
),
|
|
171
|
+
CALL_METHOD: handle_call_method,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class Untracable:
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class TraceException(BaseException):
|
|
180
|
+
# We extend BaseException instead of Exception, because it won't be considered a
|
|
181
|
+
# user-level exception by CrossHair. (this is for internal assertions)
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def check_opcode_support(opcodes: FrozenSet[int]):
|
|
186
|
+
if sys.version_info < (3, 12):
|
|
187
|
+
return
|
|
188
|
+
missing_opcodes = opcodes - set(supported_opcodes())
|
|
189
|
+
if missing_opcodes:
|
|
190
|
+
raise TraceException(
|
|
191
|
+
f"The C-level tracer does not support these opcodes: {','.join(map(dis.opname.__getitem__, missing_opcodes))}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
check_opcode_support(frozenset(_CALL_HANDLERS.keys()))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
wrapper_descriptor_type = type(int.__bool__)
|
|
199
|
+
assert str(wrapper_descriptor_type) == "<class 'wrapper_descriptor'>"
|
|
200
|
+
|
|
201
|
+
_NORMAL_CALLABLE_TYPES = (
|
|
202
|
+
type,
|
|
203
|
+
types.FunctionType, #': <class 'function'>,
|
|
204
|
+
types.MethodDescriptorType, #': <class 'method_descriptor'>,
|
|
205
|
+
types.MethodType, #': <class 'method'>,
|
|
206
|
+
types.MethodWrapperType, #': <class 'method-wrapper'>}
|
|
207
|
+
types.BuiltinFunctionType, #': <class 'builtin_function_or_method'>,
|
|
208
|
+
types.BuiltinMethodType, #: <class 'builtin_function_or_method'>,
|
|
209
|
+
types.ClassMethodDescriptorType, #': <class 'classmethod_descriptor'>,
|
|
210
|
+
wrapper_descriptor_type,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class TracingModule:
|
|
215
|
+
# override these!:
|
|
216
|
+
opcodes_wanted = frozenset(_CALL_HANDLERS.keys())
|
|
217
|
+
|
|
218
|
+
def __call__(self, frame, codeobj, opcodenum):
|
|
219
|
+
return self.trace_op(frame, codeobj, opcodenum)
|
|
220
|
+
|
|
221
|
+
def trace_op(self, frame, codeobj, opcodenum):
|
|
222
|
+
if is_tracing():
|
|
223
|
+
raise TraceException
|
|
224
|
+
call_handler = _CALL_HANDLERS.get(opcodenum)
|
|
225
|
+
if not call_handler:
|
|
226
|
+
return None
|
|
227
|
+
(fn_idx, target, kwargs_idx) = call_handler(frame)
|
|
228
|
+
binding_target = None
|
|
229
|
+
|
|
230
|
+
__self = None
|
|
231
|
+
try:
|
|
232
|
+
__self = object.__getattribute__(target, "__self__")
|
|
233
|
+
except AttributeError:
|
|
234
|
+
pass
|
|
235
|
+
if (__self is None) and (not isinstance(target, _NORMAL_CALLABLE_TYPES)):
|
|
236
|
+
try:
|
|
237
|
+
target = object.__getattribute__(target, "__call__")
|
|
238
|
+
__self = object.__getattribute__(target, "__self__")
|
|
239
|
+
except AttributeError:
|
|
240
|
+
pass
|
|
241
|
+
if __self is not None:
|
|
242
|
+
try:
|
|
243
|
+
__func = object.__getattribute__(target, "__func__")
|
|
244
|
+
except AttributeError:
|
|
245
|
+
# The implementation is likely in C.
|
|
246
|
+
# Attempt to get a function via the type:
|
|
247
|
+
typelevel_target = getattr(type(__self), target.__name__, None)
|
|
248
|
+
if typelevel_target is not None:
|
|
249
|
+
binding_target = __self
|
|
250
|
+
target = typelevel_target
|
|
251
|
+
else:
|
|
252
|
+
binding_target = __self
|
|
253
|
+
target = __func
|
|
254
|
+
|
|
255
|
+
if kwargs_idx is not None:
|
|
256
|
+
try:
|
|
257
|
+
kwargs_dict = frame_stack_read(frame, kwargs_idx)
|
|
258
|
+
except ValueError:
|
|
259
|
+
pass
|
|
260
|
+
else:
|
|
261
|
+
replacement_kwargs = {}
|
|
262
|
+
for key, val in kwargs_dict.items():
|
|
263
|
+
if isinstance(key, str):
|
|
264
|
+
replacement_kwargs[key] = val
|
|
265
|
+
continue
|
|
266
|
+
# circular import:
|
|
267
|
+
from crosshair.libimpl.builtinslib import AnySymbolicStr
|
|
268
|
+
|
|
269
|
+
if isinstance(key, AnySymbolicStr):
|
|
270
|
+
# NOTE: We need to ensure symbolic strings don't need tracing for realization
|
|
271
|
+
replacement_kwargs[key.__ch_realize__()] = val
|
|
272
|
+
else:
|
|
273
|
+
raise TypeError("keywords must be strings")
|
|
274
|
+
frame_stack_write(frame, kwargs_idx, replacement_kwargs)
|
|
275
|
+
|
|
276
|
+
if isinstance(target, Untracable):
|
|
277
|
+
return None
|
|
278
|
+
replacement = self.trace_call(frame, target, binding_target)
|
|
279
|
+
if replacement is not None:
|
|
280
|
+
target = replacement
|
|
281
|
+
if binding_target is None:
|
|
282
|
+
overwrite_target = target
|
|
283
|
+
else:
|
|
284
|
+
# re-bind a function object if it was originally a bound method
|
|
285
|
+
# on the stack.
|
|
286
|
+
overwrite_target = target.__get__(binding_target, binding_target.__class__) # type: ignore
|
|
287
|
+
frame_stack_write(frame, fn_idx, overwrite_target)
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
def trace_call(
|
|
291
|
+
self,
|
|
292
|
+
frame: Any,
|
|
293
|
+
fn: Callable,
|
|
294
|
+
binding_target: object,
|
|
295
|
+
) -> Optional[Callable]:
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
TracerConfig = Tuple[Tuple[TracingModule, ...], DefaultDict[int, List[TracingModule]]]
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class PatchingModule(TracingModule):
|
|
303
|
+
"""Hot-swap functions on the interpreter stack."""
|
|
304
|
+
|
|
305
|
+
def __init__(
|
|
306
|
+
self,
|
|
307
|
+
overrides: Optional[Dict[Callable, Callable]] = None,
|
|
308
|
+
):
|
|
309
|
+
# NOTE: you might imagine that we should use an IdKeyedDict for self.overrides
|
|
310
|
+
# However, some builtin bound methods have no way to get identity for their code:
|
|
311
|
+
#
|
|
312
|
+
# >>> float.fromhex is float.fromhex
|
|
313
|
+
# False
|
|
314
|
+
#
|
|
315
|
+
self.overrides: Dict[Callable, Callable] = {}
|
|
316
|
+
self.nextfn: Dict[object, Callable] = {} # code object to next, lower layer
|
|
317
|
+
if overrides:
|
|
318
|
+
self.add(overrides)
|
|
319
|
+
|
|
320
|
+
def add(self, new_overrides: Dict[Callable, Callable]):
|
|
321
|
+
for orig, new_override in new_overrides.items():
|
|
322
|
+
prev_override = self.overrides.get(orig, orig)
|
|
323
|
+
assert (
|
|
324
|
+
prev_override is not new_override
|
|
325
|
+
), f"Function patch {new_override} has already been applied"
|
|
326
|
+
self.nextfn[(new_override.__code__, orig)] = prev_override
|
|
327
|
+
self.overrides[orig] = new_override
|
|
328
|
+
|
|
329
|
+
def pop(self, overrides: Dict[Callable, Callable]):
|
|
330
|
+
for orig, the_override in overrides.items():
|
|
331
|
+
assert self.overrides[orig] is the_override
|
|
332
|
+
self.overrides[orig] = self.nextfn.pop((the_override.__code__, orig))
|
|
333
|
+
|
|
334
|
+
def __repr__(self):
|
|
335
|
+
return f"PatchingModule({list(self.overrides.keys())})"
|
|
336
|
+
|
|
337
|
+
def trace_call(
|
|
338
|
+
self,
|
|
339
|
+
frame: Any,
|
|
340
|
+
fn: Callable,
|
|
341
|
+
binding_target: object,
|
|
342
|
+
) -> Optional[Callable]:
|
|
343
|
+
try:
|
|
344
|
+
target = self.overrides.get(fn)
|
|
345
|
+
except TypeError as exc:
|
|
346
|
+
# The function is not hashable.
|
|
347
|
+
# This can happen when attempting to invoke a non-function,
|
|
348
|
+
# or possibly it is a method on a non-hashable object that was
|
|
349
|
+
# not properly unbound by `TracingModule.trace_op`.
|
|
350
|
+
return None
|
|
351
|
+
if target is None:
|
|
352
|
+
return None
|
|
353
|
+
caller_code = frame.f_code
|
|
354
|
+
if caller_code.co_name == "_crosshair_wrapper":
|
|
355
|
+
return None
|
|
356
|
+
target_name = getattr(fn, "__name__", "")
|
|
357
|
+
if target_name.endswith("_crosshair_wrapper"):
|
|
358
|
+
return None
|
|
359
|
+
nextfn = self.nextfn.get((caller_code, fn))
|
|
360
|
+
if nextfn is not None:
|
|
361
|
+
if nextfn is fn:
|
|
362
|
+
return None
|
|
363
|
+
return nextfn
|
|
364
|
+
return target
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class CompositeTracer:
|
|
368
|
+
def __init__(self):
|
|
369
|
+
self.ctracer = CTracer()
|
|
370
|
+
self.patching_module = PatchingModule()
|
|
371
|
+
|
|
372
|
+
def get_modules(self) -> List[TracingModule]:
|
|
373
|
+
return self.ctracer.get_modules()
|
|
374
|
+
|
|
375
|
+
def set_postop_callback(self, callback, frame):
|
|
376
|
+
self.ctracer.push_postop_callback(frame, callback)
|
|
377
|
+
|
|
378
|
+
if sys.version_info >= (3, 12):
|
|
379
|
+
|
|
380
|
+
def push_module(self, module: TracingModule) -> None:
|
|
381
|
+
sys.monitoring.restart_events()
|
|
382
|
+
self.ctracer.push_module(module)
|
|
383
|
+
|
|
384
|
+
def pop_config(self, module: TracingModule) -> None:
|
|
385
|
+
self.ctracer.pop_module(module)
|
|
386
|
+
|
|
387
|
+
def __enter__(self) -> object:
|
|
388
|
+
self.ctracer.push_module(self.patching_module)
|
|
389
|
+
tool_id = SYS_MONITORING_TOOL_ID
|
|
390
|
+
sys.monitoring.use_tool_id(tool_id, "CrossHair")
|
|
391
|
+
sys.monitoring.register_callback(
|
|
392
|
+
tool_id,
|
|
393
|
+
sys.monitoring.events.INSTRUCTION,
|
|
394
|
+
self.ctracer.instruction_monitor,
|
|
395
|
+
)
|
|
396
|
+
sys.monitoring.set_events(tool_id, sys.monitoring.events.INSTRUCTION)
|
|
397
|
+
sys.monitoring.restart_events()
|
|
398
|
+
self.ctracer.start()
|
|
399
|
+
assert not self.ctracer.is_handling()
|
|
400
|
+
assert self.ctracer.enabled()
|
|
401
|
+
return self
|
|
402
|
+
|
|
403
|
+
def __exit__(self, _etype, exc, _etb):
|
|
404
|
+
tool_id = SYS_MONITORING_TOOL_ID
|
|
405
|
+
sys.monitoring.register_callback(
|
|
406
|
+
tool_id, sys.monitoring.events.INSTRUCTION, None
|
|
407
|
+
)
|
|
408
|
+
sys.monitoring.free_tool_id(tool_id)
|
|
409
|
+
self.ctracer.stop()
|
|
410
|
+
self.ctracer.pop_module(self.patching_module)
|
|
411
|
+
|
|
412
|
+
def trace_caller(self):
|
|
413
|
+
pass
|
|
414
|
+
|
|
415
|
+
else:
|
|
416
|
+
|
|
417
|
+
def push_module(self, module: TracingModule) -> None:
|
|
418
|
+
self.ctracer.push_module(module)
|
|
419
|
+
|
|
420
|
+
def pop_config(self, module: TracingModule) -> None:
|
|
421
|
+
self.ctracer.pop_module(module)
|
|
422
|
+
|
|
423
|
+
def __enter__(self) -> object:
|
|
424
|
+
self.old_traceobj = sys.gettrace()
|
|
425
|
+
# Enable opcode tracing before setting trace function, since Python 3.12; see https://github.com/python/cpython/issues/103615
|
|
426
|
+
sys._getframe().f_trace_opcodes = True
|
|
427
|
+
self.ctracer.push_module(self.patching_module)
|
|
428
|
+
self.ctracer.start()
|
|
429
|
+
assert not self.ctracer.is_handling()
|
|
430
|
+
assert self.ctracer.enabled()
|
|
431
|
+
return self
|
|
432
|
+
|
|
433
|
+
def __exit__(self, _etype, exc, _etb):
|
|
434
|
+
self.ctracer.stop()
|
|
435
|
+
self.ctracer.pop_module(self.patching_module)
|
|
436
|
+
sys.settrace(self.old_traceobj)
|
|
437
|
+
|
|
438
|
+
def trace_caller(self):
|
|
439
|
+
# Frame 0 is the trace_caller method itself
|
|
440
|
+
# Frame 1 is the frame requesting its caller be traced
|
|
441
|
+
# Frame 2 is the caller that we're targeting
|
|
442
|
+
frame = _getframe(2)
|
|
443
|
+
frame.f_trace_opcodes = True
|
|
444
|
+
frame.f_trace = self.ctracer
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# We expect the composite tracer to be used like a singleton.
|
|
448
|
+
# (you can only have one tracer active at a time anyway)
|
|
449
|
+
# TODO: Thread-unsafe global. Make this a thread local?
|
|
450
|
+
COMPOSITE_TRACER = CompositeTracer()
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@dataclasses.dataclass
|
|
454
|
+
class CoverageResult:
|
|
455
|
+
offsets_covered: Set[int]
|
|
456
|
+
all_offsets: Set[int]
|
|
457
|
+
opcode_coverage: float
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
class CoverageTracingModule(TracingModule):
|
|
461
|
+
opcodes_wanted = frozenset(i for i in range(256))
|
|
462
|
+
|
|
463
|
+
# TODO: this needs to be moved into a separate kind of monitor to
|
|
464
|
+
# support threading (sys.monitoring probes are global)
|
|
465
|
+
|
|
466
|
+
def __init__(self, *fns: Callable):
|
|
467
|
+
assert not is_tracing()
|
|
468
|
+
self.fns = fns
|
|
469
|
+
self.codeobjects = set(fn.__code__ for fn in fns)
|
|
470
|
+
self.opcode_offsets = {
|
|
471
|
+
code: set(i.offset for i in dis.get_instructions(code))
|
|
472
|
+
for code in self.codeobjects
|
|
473
|
+
}
|
|
474
|
+
self.offsets_seen: Dict[CodeType, Set[int]] = defaultdict(set)
|
|
475
|
+
|
|
476
|
+
def trace_op(self, frame, codeobj, opcodenum):
|
|
477
|
+
code = frame.f_code
|
|
478
|
+
if code not in self.codeobjects:
|
|
479
|
+
return
|
|
480
|
+
lasti = frame.f_lasti
|
|
481
|
+
assert lasti in self.opcode_offsets[code]
|
|
482
|
+
self.offsets_seen[code].add(lasti)
|
|
483
|
+
|
|
484
|
+
def get_results(self, fn: Optional[Callable] = None):
|
|
485
|
+
if fn is None:
|
|
486
|
+
assert len(self.fns) == 1
|
|
487
|
+
fn = self.fns[0]
|
|
488
|
+
possible = self.opcode_offsets[fn.__code__]
|
|
489
|
+
seen = self.offsets_seen[fn.__code__]
|
|
490
|
+
return CoverageResult(
|
|
491
|
+
offsets_covered=seen,
|
|
492
|
+
all_offsets=possible,
|
|
493
|
+
opcode_coverage=len(seen) / len(possible),
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
class PushedModule:
|
|
498
|
+
def __init__(self, module: TracingModule):
|
|
499
|
+
self.module = module
|
|
500
|
+
|
|
501
|
+
def __enter__(self):
|
|
502
|
+
COMPOSITE_TRACER.push_module(self.module)
|
|
503
|
+
|
|
504
|
+
def __exit__(self, *a):
|
|
505
|
+
COMPOSITE_TRACER.pop_config(self.module)
|
|
506
|
+
return False
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def is_tracing():
|
|
510
|
+
return COMPOSITE_TRACER.ctracer.enabled()
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def NoTracing():
|
|
514
|
+
return TraceSwap(COMPOSITE_TRACER.ctracer, True)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
if CROSSHAIR_EXTRA_ASSERTS:
|
|
518
|
+
|
|
519
|
+
def ResumedTracing():
|
|
520
|
+
if COMPOSITE_TRACER.ctracer.is_handling():
|
|
521
|
+
raise TraceException("Cannot resume tracing while opcode handling")
|
|
522
|
+
return TraceSwap(COMPOSITE_TRACER.ctracer, False)
|
|
523
|
+
|
|
524
|
+
else:
|
|
525
|
+
|
|
526
|
+
def ResumedTracing():
|
|
527
|
+
return TraceSwap(COMPOSITE_TRACER.ctracer, False)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
_T = TypeVar("_T")
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def tracing_iter(itr: Iterable[_T]) -> Iterable[_T]:
|
|
534
|
+
"""Selectively re-enable tracing only during iteration."""
|
|
535
|
+
assert not is_tracing()
|
|
536
|
+
# TODO: should we protect his line with ResumedTracing() too?:
|
|
537
|
+
itr = iter(itr)
|
|
538
|
+
while True:
|
|
539
|
+
try:
|
|
540
|
+
with ResumedTracing():
|
|
541
|
+
value = next(itr)
|
|
542
|
+
except StopIteration:
|
|
543
|
+
return
|
|
544
|
+
yield value
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import dis
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from crosshair.tracers import (
|
|
6
|
+
COMPOSITE_TRACER,
|
|
7
|
+
CompositeTracer,
|
|
8
|
+
CoverageTracingModule,
|
|
9
|
+
PatchingModule,
|
|
10
|
+
PushedModule,
|
|
11
|
+
TraceSwap,
|
|
12
|
+
TracingModule,
|
|
13
|
+
is_tracing,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def overridefn(*a, **kw):
|
|
18
|
+
assert a[0] == 42
|
|
19
|
+
return 2
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def examplefn(x: int, *a, **kw) -> int:
|
|
23
|
+
return 1
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def overridemethod(*a, **kw):
|
|
27
|
+
assert a[1] == 42
|
|
28
|
+
return 2
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Example:
|
|
32
|
+
def example_method(self, a: int, **kw) -> int:
|
|
33
|
+
return 1
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
tracer = CompositeTracer()
|
|
37
|
+
|
|
38
|
+
tracer.push_module(
|
|
39
|
+
PatchingModule(
|
|
40
|
+
{
|
|
41
|
+
examplefn: overridefn,
|
|
42
|
+
Example.__dict__["example_method"]: overridemethod,
|
|
43
|
+
tuple.__len__: (lambda a: 42),
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.fixture(autouse=True)
|
|
50
|
+
def check_tracer_state():
|
|
51
|
+
assert not is_tracing()
|
|
52
|
+
assert not tracer.ctracer.enabled()
|
|
53
|
+
yield None
|
|
54
|
+
assert not is_tracing()
|
|
55
|
+
assert not tracer.ctracer.enabled()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_CALL_FUNCTION():
|
|
59
|
+
with tracer:
|
|
60
|
+
assert examplefn(42) == 2
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_CALL_FUNCTION_KW():
|
|
64
|
+
with tracer:
|
|
65
|
+
assert examplefn(42, option=1) == 2
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_CALL_FUNCTION_EX():
|
|
69
|
+
with tracer:
|
|
70
|
+
a = (42, 1, 2, 3)
|
|
71
|
+
assert examplefn(*a, option=1) == 2
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_CALL_METHOD():
|
|
75
|
+
with tracer:
|
|
76
|
+
assert Example().example_method(42) == 2
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_override_method_in_c():
|
|
80
|
+
with tracer:
|
|
81
|
+
assert (1, 2, 3).__len__() == 42
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_no_tracing():
|
|
85
|
+
with tracer:
|
|
86
|
+
# TraceSwap(tracer.ctracer, True) is the same as NoTracing() for `tracer`:
|
|
87
|
+
with TraceSwap(tracer.ctracer, True):
|
|
88
|
+
assert (1, 2, 3).__len__() == 3
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_measure_fn_coverage() -> None:
|
|
92
|
+
def called_by_foo(x: int) -> int:
|
|
93
|
+
return x
|
|
94
|
+
|
|
95
|
+
def foo(x: int) -> int:
|
|
96
|
+
if called_by_foo(x) < 50:
|
|
97
|
+
return x
|
|
98
|
+
else:
|
|
99
|
+
return (x - 50) + (called_by_foo(2 + 1) > 3) + -abs(x)
|
|
100
|
+
|
|
101
|
+
def calls_foo(x: int) -> int:
|
|
102
|
+
return foo(x)
|
|
103
|
+
|
|
104
|
+
cov1 = CoverageTracingModule(foo)
|
|
105
|
+
cov2 = CoverageTracingModule(foo)
|
|
106
|
+
cov3 = CoverageTracingModule(foo)
|
|
107
|
+
with COMPOSITE_TRACER:
|
|
108
|
+
with PushedModule(cov1):
|
|
109
|
+
calls_foo(5)
|
|
110
|
+
|
|
111
|
+
with PushedModule(cov2):
|
|
112
|
+
calls_foo(100)
|
|
113
|
+
|
|
114
|
+
with PushedModule(cov3):
|
|
115
|
+
calls_foo(5)
|
|
116
|
+
calls_foo(100)
|
|
117
|
+
|
|
118
|
+
assert 0.4 > cov1.get_results().opcode_coverage > 0.1
|
|
119
|
+
assert 0.95 > cov2.get_results().opcode_coverage > 0.6
|
|
120
|
+
# Note that we can't get 100% - there's an extra "return None"
|
|
121
|
+
# at the end that's unreachable.
|
|
122
|
+
assert cov3.get_results().opcode_coverage > 0.85
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class Explode(ValueError):
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ExplodingModule(TracingModule):
|
|
130
|
+
opcodes_wanted = frozenset(
|
|
131
|
+
[
|
|
132
|
+
dis.opmap.get("BINARY_ADD", 256),
|
|
133
|
+
dis.opmap.get("BINARY_OP", 256), # on >3.11
|
|
134
|
+
]
|
|
135
|
+
)
|
|
136
|
+
was_called = False
|
|
137
|
+
|
|
138
|
+
def __call__(self, frame, codeobj, codenum):
|
|
139
|
+
self.was_called = True
|
|
140
|
+
raise Explode("I explode")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_tracer_propagates_errors():
|
|
144
|
+
mod = ExplodingModule()
|
|
145
|
+
COMPOSITE_TRACER.push_module(mod)
|
|
146
|
+
try:
|
|
147
|
+
with COMPOSITE_TRACER:
|
|
148
|
+
x, y = 1, 3
|
|
149
|
+
print(x + y)
|
|
150
|
+
except Explode:
|
|
151
|
+
pass
|
|
152
|
+
else:
|
|
153
|
+
assert mod.was_called
|
|
154
|
+
COMPOSITE_TRACER.pop_config(mod)
|