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.
Files changed (176) hide show
  1. _crosshair_tracers.cpython-312-darwin.so +0 -0
  2. crosshair/__init__.py +42 -0
  3. crosshair/__main__.py +8 -0
  4. crosshair/_mark_stacks.h +790 -0
  5. crosshair/_preliminaries_test.py +18 -0
  6. crosshair/_tracers.h +94 -0
  7. crosshair/_tracers_pycompat.h +522 -0
  8. crosshair/_tracers_test.py +138 -0
  9. crosshair/abcstring.py +245 -0
  10. crosshair/auditwall.py +190 -0
  11. crosshair/auditwall_test.py +77 -0
  12. crosshair/codeconfig.py +113 -0
  13. crosshair/codeconfig_test.py +117 -0
  14. crosshair/condition_parser.py +1237 -0
  15. crosshair/condition_parser_test.py +497 -0
  16. crosshair/conftest.py +30 -0
  17. crosshair/copyext.py +155 -0
  18. crosshair/copyext_test.py +84 -0
  19. crosshair/core.py +1763 -0
  20. crosshair/core_and_libs.py +149 -0
  21. crosshair/core_regestered_types_test.py +82 -0
  22. crosshair/core_test.py +1316 -0
  23. crosshair/diff_behavior.py +314 -0
  24. crosshair/diff_behavior_test.py +261 -0
  25. crosshair/dynamic_typing.py +346 -0
  26. crosshair/dynamic_typing_test.py +210 -0
  27. crosshair/enforce.py +282 -0
  28. crosshair/enforce_test.py +182 -0
  29. crosshair/examples/PEP316/__init__.py +1 -0
  30. crosshair/examples/PEP316/bugs_detected/__init__.py +0 -0
  31. crosshair/examples/PEP316/bugs_detected/getattr_magic.py +16 -0
  32. crosshair/examples/PEP316/bugs_detected/hash_consistent_with_equals.py +31 -0
  33. crosshair/examples/PEP316/bugs_detected/shopping_cart.py +24 -0
  34. crosshair/examples/PEP316/bugs_detected/showcase.py +39 -0
  35. crosshair/examples/PEP316/correct_code/__init__.py +0 -0
  36. crosshair/examples/PEP316/correct_code/arith.py +60 -0
  37. crosshair/examples/PEP316/correct_code/chess.py +77 -0
  38. crosshair/examples/PEP316/correct_code/nesting_inference.py +17 -0
  39. crosshair/examples/PEP316/correct_code/numpy_examples.py +132 -0
  40. crosshair/examples/PEP316/correct_code/rolling_average.py +35 -0
  41. crosshair/examples/PEP316/correct_code/showcase.py +104 -0
  42. crosshair/examples/__init__.py +0 -0
  43. crosshair/examples/check_examples_test.py +146 -0
  44. crosshair/examples/deal/__init__.py +1 -0
  45. crosshair/examples/icontract/__init__.py +1 -0
  46. crosshair/examples/icontract/bugs_detected/__init__.py +0 -0
  47. crosshair/examples/icontract/bugs_detected/showcase.py +41 -0
  48. crosshair/examples/icontract/bugs_detected/wrong_sign.py +8 -0
  49. crosshair/examples/icontract/correct_code/__init__.py +0 -0
  50. crosshair/examples/icontract/correct_code/arith.py +51 -0
  51. crosshair/examples/icontract/correct_code/showcase.py +94 -0
  52. crosshair/fnutil.py +391 -0
  53. crosshair/fnutil_test.py +75 -0
  54. crosshair/fuzz_core_test.py +516 -0
  55. crosshair/libimpl/__init__.py +0 -0
  56. crosshair/libimpl/arraylib.py +161 -0
  57. crosshair/libimpl/binascii_ch_test.py +30 -0
  58. crosshair/libimpl/binascii_test.py +67 -0
  59. crosshair/libimpl/binasciilib.py +150 -0
  60. crosshair/libimpl/bisectlib_test.py +23 -0
  61. crosshair/libimpl/builtinslib.py +5228 -0
  62. crosshair/libimpl/builtinslib_ch_test.py +1191 -0
  63. crosshair/libimpl/builtinslib_test.py +3735 -0
  64. crosshair/libimpl/codecslib.py +86 -0
  65. crosshair/libimpl/codecslib_test.py +86 -0
  66. crosshair/libimpl/collectionslib.py +264 -0
  67. crosshair/libimpl/collectionslib_ch_test.py +252 -0
  68. crosshair/libimpl/collectionslib_test.py +332 -0
  69. crosshair/libimpl/copylib.py +23 -0
  70. crosshair/libimpl/copylib_test.py +18 -0
  71. crosshair/libimpl/datetimelib.py +2559 -0
  72. crosshair/libimpl/datetimelib_ch_test.py +354 -0
  73. crosshair/libimpl/datetimelib_test.py +112 -0
  74. crosshair/libimpl/decimallib.py +5257 -0
  75. crosshair/libimpl/decimallib_ch_test.py +78 -0
  76. crosshair/libimpl/decimallib_test.py +76 -0
  77. crosshair/libimpl/encodings/__init__.py +23 -0
  78. crosshair/libimpl/encodings/_encutil.py +187 -0
  79. crosshair/libimpl/encodings/ascii.py +44 -0
  80. crosshair/libimpl/encodings/latin_1.py +40 -0
  81. crosshair/libimpl/encodings/utf_8.py +93 -0
  82. crosshair/libimpl/encodings_ch_test.py +83 -0
  83. crosshair/libimpl/fractionlib.py +16 -0
  84. crosshair/libimpl/fractionlib_test.py +80 -0
  85. crosshair/libimpl/functoolslib.py +34 -0
  86. crosshair/libimpl/functoolslib_test.py +56 -0
  87. crosshair/libimpl/hashliblib.py +30 -0
  88. crosshair/libimpl/hashliblib_test.py +18 -0
  89. crosshair/libimpl/heapqlib.py +47 -0
  90. crosshair/libimpl/heapqlib_test.py +21 -0
  91. crosshair/libimpl/importliblib.py +18 -0
  92. crosshair/libimpl/importliblib_test.py +38 -0
  93. crosshair/libimpl/iolib.py +216 -0
  94. crosshair/libimpl/iolib_ch_test.py +128 -0
  95. crosshair/libimpl/iolib_test.py +19 -0
  96. crosshair/libimpl/ipaddresslib.py +8 -0
  97. crosshair/libimpl/itertoolslib.py +44 -0
  98. crosshair/libimpl/itertoolslib_test.py +44 -0
  99. crosshair/libimpl/jsonlib.py +984 -0
  100. crosshair/libimpl/jsonlib_ch_test.py +42 -0
  101. crosshair/libimpl/jsonlib_test.py +51 -0
  102. crosshair/libimpl/mathlib.py +179 -0
  103. crosshair/libimpl/mathlib_ch_test.py +44 -0
  104. crosshair/libimpl/mathlib_test.py +67 -0
  105. crosshair/libimpl/oslib.py +7 -0
  106. crosshair/libimpl/pathliblib_test.py +10 -0
  107. crosshair/libimpl/randomlib.py +178 -0
  108. crosshair/libimpl/randomlib_test.py +120 -0
  109. crosshair/libimpl/relib.py +846 -0
  110. crosshair/libimpl/relib_ch_test.py +169 -0
  111. crosshair/libimpl/relib_test.py +493 -0
  112. crosshair/libimpl/timelib.py +72 -0
  113. crosshair/libimpl/timelib_test.py +82 -0
  114. crosshair/libimpl/typeslib.py +15 -0
  115. crosshair/libimpl/typeslib_test.py +36 -0
  116. crosshair/libimpl/unicodedatalib.py +75 -0
  117. crosshair/libimpl/unicodedatalib_test.py +42 -0
  118. crosshair/libimpl/urlliblib.py +23 -0
  119. crosshair/libimpl/urlliblib_test.py +19 -0
  120. crosshair/libimpl/weakreflib.py +13 -0
  121. crosshair/libimpl/weakreflib_test.py +69 -0
  122. crosshair/libimpl/zliblib.py +15 -0
  123. crosshair/libimpl/zliblib_test.py +13 -0
  124. crosshair/lsp_server.py +261 -0
  125. crosshair/lsp_server_test.py +30 -0
  126. crosshair/main.py +973 -0
  127. crosshair/main_test.py +543 -0
  128. crosshair/objectproxy.py +376 -0
  129. crosshair/objectproxy_test.py +41 -0
  130. crosshair/opcode_intercept.py +601 -0
  131. crosshair/opcode_intercept_test.py +304 -0
  132. crosshair/options.py +218 -0
  133. crosshair/options_test.py +10 -0
  134. crosshair/patch_equivalence_test.py +75 -0
  135. crosshair/path_cover.py +209 -0
  136. crosshair/path_cover_test.py +138 -0
  137. crosshair/path_search.py +161 -0
  138. crosshair/path_search_test.py +52 -0
  139. crosshair/pathing_oracle.py +271 -0
  140. crosshair/pathing_oracle_test.py +21 -0
  141. crosshair/pure_importer.py +27 -0
  142. crosshair/pure_importer_test.py +16 -0
  143. crosshair/py.typed +0 -0
  144. crosshair/register_contract.py +273 -0
  145. crosshair/register_contract_test.py +190 -0
  146. crosshair/simplestructs.py +1165 -0
  147. crosshair/simplestructs_test.py +283 -0
  148. crosshair/smtlib.py +24 -0
  149. crosshair/smtlib_test.py +14 -0
  150. crosshair/statespace.py +1199 -0
  151. crosshair/statespace_test.py +108 -0
  152. crosshair/stubs_parser.py +352 -0
  153. crosshair/stubs_parser_test.py +43 -0
  154. crosshair/test_util.py +329 -0
  155. crosshair/test_util_test.py +26 -0
  156. crosshair/tools/__init__.py +0 -0
  157. crosshair/tools/check_help_in_doc.py +264 -0
  158. crosshair/tools/check_init_and_setup_coincide.py +119 -0
  159. crosshair/tools/generate_demo_table.py +127 -0
  160. crosshair/tracers.py +544 -0
  161. crosshair/tracers_test.py +154 -0
  162. crosshair/type_repo.py +151 -0
  163. crosshair/unicode_categories.py +589 -0
  164. crosshair/unicode_categories_test.py +27 -0
  165. crosshair/util.py +741 -0
  166. crosshair/util_test.py +173 -0
  167. crosshair/watcher.py +307 -0
  168. crosshair/watcher_test.py +107 -0
  169. crosshair/z3util.py +76 -0
  170. crosshair/z3util_test.py +11 -0
  171. crosshair_tool-0.0.99.dist-info/METADATA +144 -0
  172. crosshair_tool-0.0.99.dist-info/RECORD +176 -0
  173. crosshair_tool-0.0.99.dist-info/WHEEL +6 -0
  174. crosshair_tool-0.0.99.dist-info/entry_points.txt +3 -0
  175. crosshair_tool-0.0.99.dist-info/licenses/LICENSE +93 -0
  176. 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)