crosshair-tool 0.0.56__cp39-cp39-macosx_11_0_arm64.whl → 0.0.100__cp39-cp39-macosx_11_0_arm64.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-39-darwin.so +0 -0
- crosshair/__init__.py +1 -1
- crosshair/_mark_stacks.h +51 -24
- crosshair/_tracers.h +9 -5
- crosshair/_tracers_test.py +19 -9
- crosshair/auditwall.py +9 -8
- crosshair/auditwall_test.py +31 -19
- crosshair/codeconfig.py +3 -2
- crosshair/condition_parser.py +17 -133
- crosshair/condition_parser_test.py +54 -96
- crosshair/conftest.py +1 -1
- crosshair/copyext.py +91 -22
- crosshair/copyext_test.py +33 -0
- crosshair/core.py +259 -203
- crosshair/core_and_libs.py +20 -0
- crosshair/core_regestered_types_test.py +82 -0
- crosshair/core_test.py +693 -664
- crosshair/diff_behavior.py +76 -21
- crosshair/diff_behavior_test.py +132 -23
- crosshair/dynamic_typing.py +128 -18
- crosshair/dynamic_typing_test.py +91 -4
- crosshair/enforce.py +1 -6
- crosshair/enforce_test.py +15 -23
- crosshair/examples/check_examples_test.py +2 -1
- crosshair/fnutil.py +2 -3
- crosshair/fnutil_test.py +0 -7
- crosshair/fuzz_core_test.py +70 -83
- crosshair/libimpl/arraylib.py +10 -7
- 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 +5 -5
- crosshair/libimpl/builtinslib.py +1002 -682
- crosshair/libimpl/builtinslib_ch_test.py +108 -30
- crosshair/libimpl/builtinslib_test.py +431 -143
- crosshair/libimpl/codecslib.py +22 -2
- crosshair/libimpl/codecslib_test.py +41 -9
- crosshair/libimpl/collectionslib.py +44 -8
- crosshair/libimpl/collectionslib_test.py +108 -20
- crosshair/libimpl/copylib.py +1 -1
- crosshair/libimpl/copylib_test.py +18 -0
- crosshair/libimpl/datetimelib.py +84 -67
- crosshair/libimpl/datetimelib_ch_test.py +12 -7
- crosshair/libimpl/datetimelib_test.py +5 -6
- crosshair/libimpl/decimallib.py +5257 -0
- crosshair/libimpl/decimallib_ch_test.py +78 -0
- crosshair/libimpl/decimallib_test.py +76 -0
- crosshair/libimpl/encodings/_encutil.py +21 -11
- crosshair/libimpl/fractionlib.py +16 -0
- crosshair/libimpl/fractionlib_test.py +80 -0
- crosshair/libimpl/functoolslib.py +19 -7
- crosshair/libimpl/functoolslib_test.py +22 -6
- crosshair/libimpl/hashliblib.py +30 -0
- crosshair/libimpl/hashliblib_test.py +18 -0
- crosshair/libimpl/heapqlib.py +32 -5
- crosshair/libimpl/heapqlib_test.py +15 -12
- crosshair/libimpl/iolib.py +7 -4
- crosshair/libimpl/ipaddresslib.py +8 -0
- crosshair/libimpl/itertoolslib_test.py +1 -1
- crosshair/libimpl/mathlib.py +165 -2
- crosshair/libimpl/mathlib_ch_test.py +44 -0
- crosshair/libimpl/mathlib_test.py +59 -16
- crosshair/libimpl/oslib.py +7 -0
- crosshair/libimpl/pathliblib_test.py +10 -0
- crosshair/libimpl/randomlib.py +1 -0
- crosshair/libimpl/randomlib_test.py +6 -4
- crosshair/libimpl/relib.py +180 -59
- crosshair/libimpl/relib_ch_test.py +26 -2
- crosshair/libimpl/relib_test.py +77 -14
- crosshair/libimpl/timelib.py +35 -13
- crosshair/libimpl/timelib_test.py +13 -3
- crosshair/libimpl/typeslib.py +15 -0
- crosshair/libimpl/typeslib_test.py +36 -0
- crosshair/libimpl/unicodedatalib_test.py +3 -3
- 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 +21 -10
- crosshair/main.py +48 -28
- crosshair/main_test.py +59 -14
- crosshair/objectproxy.py +39 -14
- crosshair/objectproxy_test.py +27 -13
- crosshair/opcode_intercept.py +212 -24
- crosshair/opcode_intercept_test.py +172 -18
- crosshair/options.py +0 -1
- crosshair/patch_equivalence_test.py +5 -21
- crosshair/path_cover.py +7 -5
- crosshair/path_search.py +6 -4
- crosshair/path_search_test.py +1 -2
- crosshair/pathing_oracle.py +53 -10
- crosshair/pathing_oracle_test.py +21 -0
- crosshair/pure_importer_test.py +5 -21
- crosshair/register_contract.py +16 -6
- crosshair/register_contract_test.py +2 -14
- crosshair/simplestructs.py +154 -85
- crosshair/simplestructs_test.py +16 -2
- crosshair/smtlib.py +24 -0
- crosshair/smtlib_test.py +14 -0
- crosshair/statespace.py +319 -196
- crosshair/statespace_test.py +45 -0
- crosshair/stubs_parser.py +0 -2
- crosshair/test_util.py +87 -25
- crosshair/test_util_test.py +26 -0
- crosshair/tools/check_init_and_setup_coincide.py +0 -3
- crosshair/tools/generate_demo_table.py +2 -2
- crosshair/tracers.py +141 -49
- crosshair/type_repo.py +11 -4
- crosshair/unicode_categories.py +1 -0
- crosshair/util.py +158 -76
- crosshair/util_test.py +13 -20
- crosshair/watcher.py +4 -4
- crosshair/z3util.py +1 -1
- {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/METADATA +45 -36
- crosshair_tool-0.0.100.dist-info/RECORD +176 -0
- {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/WHEEL +2 -1
- crosshair/examples/hypothesis/__init__.py +0 -2
- crosshair/examples/hypothesis/bugs_detected/simple_strategies.py +0 -74
- crosshair_tool-0.0.56.dist-info/RECORD +0 -152
- /crosshair/{examples/hypothesis/bugs_detected/__init__.py → py.typed} +0 -0
- {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/entry_points.txt +0 -0
- {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info/licenses}/LICENSE +0 -0
- {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/top_level.txt +0 -0
crosshair/core.py
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
import enum
|
|
12
12
|
import functools
|
|
13
13
|
import inspect
|
|
14
|
-
import itertools
|
|
15
14
|
import linecache
|
|
16
15
|
import os.path
|
|
17
16
|
import sys
|
|
@@ -23,6 +22,8 @@ from collections import ChainMap, defaultdict, deque
|
|
|
23
22
|
from contextlib import ExitStack
|
|
24
23
|
from dataclasses import dataclass, replace
|
|
25
24
|
from inspect import BoundArguments, Signature, isabstract
|
|
25
|
+
from time import monotonic
|
|
26
|
+
from traceback import StackSummary, extract_stack, extract_tb, format_exc
|
|
26
27
|
from typing import (
|
|
27
28
|
Any,
|
|
28
29
|
Callable,
|
|
@@ -33,11 +34,9 @@ from typing import (
|
|
|
33
34
|
List,
|
|
34
35
|
Mapping,
|
|
35
36
|
MutableMapping,
|
|
36
|
-
NewType,
|
|
37
37
|
Optional,
|
|
38
38
|
Sequence,
|
|
39
39
|
Set,
|
|
40
|
-
Sized,
|
|
41
40
|
Tuple,
|
|
42
41
|
Type,
|
|
43
42
|
TypeVar,
|
|
@@ -73,12 +72,11 @@ from crosshair.fnutil import (
|
|
|
73
72
|
resolve_signature,
|
|
74
73
|
)
|
|
75
74
|
from crosshair.options import DEFAULT_OPTIONS, AnalysisOptions, AnalysisOptionSet
|
|
76
|
-
from crosshair.register_contract import get_contract
|
|
75
|
+
from crosshair.register_contract import clear_contract_registrations, get_contract
|
|
77
76
|
from crosshair.statespace import (
|
|
78
77
|
AnalysisMessage,
|
|
79
78
|
CallAnalysis,
|
|
80
79
|
MessageType,
|
|
81
|
-
NotDeterministic,
|
|
82
80
|
RootNode,
|
|
83
81
|
SimpleStateSpace,
|
|
84
82
|
StateSpace,
|
|
@@ -95,6 +93,7 @@ from crosshair.tracers import (
|
|
|
95
93
|
PatchingModule,
|
|
96
94
|
ResumedTracing,
|
|
97
95
|
TracingModule,
|
|
96
|
+
check_opcode_support,
|
|
98
97
|
is_tracing,
|
|
99
98
|
)
|
|
100
99
|
from crosshair.type_repo import get_subclass_map
|
|
@@ -102,13 +101,16 @@ from crosshair.util import (
|
|
|
102
101
|
ATOMIC_IMMUTABLE_TYPES,
|
|
103
102
|
UNABLE_TO_REPR_TEXT,
|
|
104
103
|
AttributeHolder,
|
|
105
|
-
|
|
104
|
+
CrossHairInternal,
|
|
106
105
|
CrosshairUnsupported,
|
|
106
|
+
CrossHairValue,
|
|
107
107
|
EvalFriendlyReprContext,
|
|
108
108
|
IdKeyedDict,
|
|
109
109
|
IgnoreAttempt,
|
|
110
|
+
NotDeterministic,
|
|
110
111
|
ReferencedIdentifier,
|
|
111
112
|
UnexploredPath,
|
|
113
|
+
ch_stack,
|
|
112
114
|
debug,
|
|
113
115
|
eval_friendly_repr,
|
|
114
116
|
format_boundargs,
|
|
@@ -121,28 +123,31 @@ from crosshair.util import (
|
|
|
121
123
|
samefile,
|
|
122
124
|
smtlib_typename,
|
|
123
125
|
sourcelines,
|
|
124
|
-
test_stack,
|
|
125
126
|
type_args_of,
|
|
126
127
|
warn,
|
|
127
128
|
)
|
|
128
129
|
|
|
130
|
+
if sys.version_info >= (3, 12):
|
|
131
|
+
from typing import TypeAliasType
|
|
132
|
+
|
|
133
|
+
TypeAliasTypes = (TypeAliasType,)
|
|
134
|
+
else:
|
|
135
|
+
TypeAliasTypes = ()
|
|
136
|
+
|
|
137
|
+
|
|
129
138
|
_MISSING = object()
|
|
130
139
|
|
|
131
140
|
|
|
132
141
|
_OPCODE_PATCHES: List[TracingModule] = []
|
|
133
142
|
|
|
134
143
|
_PATCH_REGISTRATIONS: Dict[Callable, Callable] = {}
|
|
135
|
-
_PATCH_FN_TYPE_REGISTRATIONS: Dict[type, Callable] = {}
|
|
136
144
|
|
|
137
145
|
|
|
138
|
-
class Patched
|
|
146
|
+
class Patched:
|
|
139
147
|
def __enter__(self):
|
|
140
148
|
COMPOSITE_TRACER.patching_module.add(_PATCH_REGISTRATIONS)
|
|
141
|
-
COMPOSITE_TRACER.patching_module.fn_type_overrides = (
|
|
142
|
-
_PATCH_FN_TYPE_REGISTRATIONS
|
|
143
|
-
)
|
|
144
149
|
if len(_OPCODE_PATCHES) == 0:
|
|
145
|
-
raise
|
|
150
|
+
raise CrossHairInternal("Opcode patches haven't been loaded yet.")
|
|
146
151
|
for module in _OPCODE_PATCHES:
|
|
147
152
|
COMPOSITE_TRACER.push_module(module)
|
|
148
153
|
self.pushed = _OPCODE_PATCHES[:]
|
|
@@ -152,7 +157,6 @@ class Patched(TracingModule):
|
|
|
152
157
|
for module in reversed(self.pushed):
|
|
153
158
|
COMPOSITE_TRACER.pop_config(module)
|
|
154
159
|
COMPOSITE_TRACER.patching_module.pop(_PATCH_REGISTRATIONS)
|
|
155
|
-
COMPOSITE_TRACER.patching_module.fn_type_overrides = {}
|
|
156
160
|
return False
|
|
157
161
|
|
|
158
162
|
|
|
@@ -174,11 +178,46 @@ class _StandaloneStatespace(ExitStack):
|
|
|
174
178
|
standalone_statespace = _StandaloneStatespace()
|
|
175
179
|
|
|
176
180
|
|
|
181
|
+
def suspected_proxy_intolerance_exception(exc_value: Exception) -> bool:
|
|
182
|
+
# NOTE: this is an intentionally very hacky function that is used to
|
|
183
|
+
# skip iterations where a symbolic is used in some function that can't
|
|
184
|
+
# accept it.
|
|
185
|
+
# As the standard library gets more and more support, this is
|
|
186
|
+
# less necessary.
|
|
187
|
+
# Although it would still provide value for 3rd party libraries
|
|
188
|
+
# implemented in C, the long-term goal is to remove it and just let
|
|
189
|
+
# CrossHair be noisy where it isn't supported.
|
|
190
|
+
|
|
191
|
+
if not isinstance(exc_value, TypeError):
|
|
192
|
+
return False
|
|
193
|
+
exc_str = str(exc_value)
|
|
194
|
+
atomic_symbolic = "SymbolicInt" in exc_str or "SymbolicFloat" in exc_str
|
|
195
|
+
if (
|
|
196
|
+
atomic_symbolic
|
|
197
|
+
or "SymbolicStr" in exc_str
|
|
198
|
+
or "__hash__ method should return an integer" in exc_str
|
|
199
|
+
or "expected string or bytes-like object" in exc_str
|
|
200
|
+
):
|
|
201
|
+
if (
|
|
202
|
+
"can only concatenate" in exc_str
|
|
203
|
+
or "NoneType" in exc_str
|
|
204
|
+
or "object is not callable" in exc_str
|
|
205
|
+
):
|
|
206
|
+
# https://github.com/pschanely/CrossHair/issues/234
|
|
207
|
+
# (the three conditions above correspond to examples 2, 3, and 4)
|
|
208
|
+
return False
|
|
209
|
+
if atomic_symbolic and "object is not iterable" in exc_str:
|
|
210
|
+
# https://github.com/pschanely/CrossHair/issues/322
|
|
211
|
+
return False
|
|
212
|
+
return True
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
|
|
177
216
|
class ExceptionFilter:
|
|
178
217
|
analysis: CallAnalysis
|
|
179
218
|
ignore: bool = False
|
|
180
219
|
ignore_with_confirmation: bool = False
|
|
181
|
-
user_exc: Optional[Tuple[BaseException,
|
|
220
|
+
user_exc: Optional[Tuple[BaseException, StackSummary]] = None
|
|
182
221
|
expected_exceptions: Tuple[Type[BaseException], ...]
|
|
183
222
|
|
|
184
223
|
def __init__(
|
|
@@ -212,24 +251,24 @@ class ExceptionFilter:
|
|
|
212
251
|
self.ignore = True
|
|
213
252
|
self.analysis = CallAnalysis(VerificationStatus.CONFIRMED)
|
|
214
253
|
return True
|
|
215
|
-
if
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
or "SymbolicFloat" in exc_str
|
|
221
|
-
or "__hash__ method should return an integer" in exc_str
|
|
222
|
-
or "expected string or bytes-like object" in exc_str
|
|
223
|
-
):
|
|
224
|
-
# Ideally we'd attempt literal strings after encountering this.
|
|
225
|
-
# See https://github.com/pschanely/CrossHair/issues/8
|
|
226
|
-
debug("Proxy intolerace at: ", traceback.format_exc())
|
|
227
|
-
raise CrosshairUnsupported("Detected proxy intolerance: " + exc_str)
|
|
254
|
+
if suspected_proxy_intolerance_exception(exc_value):
|
|
255
|
+
# Ideally we'd attempt literal strings after encountering this.
|
|
256
|
+
# See https://github.com/pschanely/CrossHair/issues/8
|
|
257
|
+
debug("Proxy intolerace:", exc_value, "at", format_exc())
|
|
258
|
+
raise CrosshairUnsupported("Detected proxy intolerance")
|
|
228
259
|
if isinstance(exc_value, (Exception, PreconditionFailed)):
|
|
229
|
-
if isinstance(
|
|
230
|
-
|
|
260
|
+
if isinstance(
|
|
261
|
+
exc_value,
|
|
262
|
+
(
|
|
263
|
+
z3.Z3Exception, # internal issue, re-raise
|
|
264
|
+
NotDeterministic, # cannot continue to use the solver, re-raise
|
|
265
|
+
),
|
|
266
|
+
):
|
|
267
|
+
return False
|
|
231
268
|
# Most other issues are assumed to be user-facing exceptions:
|
|
232
|
-
|
|
269
|
+
lower_frames = extract_tb(sys.exc_info()[2])
|
|
270
|
+
higher_frames = extract_stack()[:-2]
|
|
271
|
+
self.user_exc = (exc_value, StackSummary(higher_frames + lower_frames))
|
|
233
272
|
self.analysis = CallAnalysis(VerificationStatus.REFUTED)
|
|
234
273
|
return True # suppress user-level exception
|
|
235
274
|
return False # re-raise resource and system issues
|
|
@@ -246,13 +285,9 @@ def realize(value: Any) -> Any:
|
|
|
246
285
|
return value
|
|
247
286
|
|
|
248
287
|
|
|
249
|
-
def deep_realize(value: _T) -> _T:
|
|
288
|
+
def deep_realize(value: _T, memo: Optional[Dict] = None) -> _T:
|
|
250
289
|
with NoTracing():
|
|
251
|
-
return deepcopyext(value, CopyMode.REALIZE, {})
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
class CrossHairValue:
|
|
255
|
-
pass
|
|
290
|
+
return deepcopyext(value, CopyMode.REALIZE, {} if memo is None else memo)
|
|
256
291
|
|
|
257
292
|
|
|
258
293
|
def normalize_pytype(typ: Type) -> Type:
|
|
@@ -278,7 +313,7 @@ def normalize_pytype(typ: Type) -> Type:
|
|
|
278
313
|
|
|
279
314
|
def python_type(o: object) -> Type:
|
|
280
315
|
if is_tracing():
|
|
281
|
-
raise
|
|
316
|
+
raise CrossHairInternal("should not be tracing while getting pytype")
|
|
282
317
|
if hasattr(type(o), "__ch_pytype__"):
|
|
283
318
|
obj_type = o.__ch_pytype__() # type: ignore
|
|
284
319
|
if hasattr(obj_type, "__origin__"):
|
|
@@ -288,17 +323,51 @@ def python_type(o: object) -> Type:
|
|
|
288
323
|
return type(o)
|
|
289
324
|
|
|
290
325
|
|
|
291
|
-
def
|
|
326
|
+
def class_with_realized_methods(cls: _T) -> _T:
|
|
327
|
+
overrides = {
|
|
328
|
+
method_name: with_realized_args(method)
|
|
329
|
+
for method_name, method in inspect.getmembers(cls)
|
|
330
|
+
if callable(method) and not method_name.startswith("_")
|
|
331
|
+
}
|
|
332
|
+
return type(cls.__name__, (cls,), overrides) # type: ignore
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def with_realized_args(fn: Callable, deep=False) -> Callable:
|
|
336
|
+
realize_fn = deep_realize if deep else realize
|
|
337
|
+
|
|
292
338
|
def realizer(*a, **kw):
|
|
293
339
|
with NoTracing():
|
|
294
|
-
a =
|
|
295
|
-
kw = {k:
|
|
296
|
-
|
|
340
|
+
a = [realize_fn(arg) for arg in a]
|
|
341
|
+
kw = {k: realize_fn(v) for (k, v) in kw.items()}
|
|
342
|
+
# You might think we don't need tracing here, but some operations can invoke user-defined behavior:
|
|
343
|
+
return fn(*a, **kw)
|
|
297
344
|
|
|
298
345
|
functools.update_wrapper(realizer, fn)
|
|
299
346
|
return realizer
|
|
300
347
|
|
|
301
348
|
|
|
349
|
+
def with_checked_self(pytype, method_name):
|
|
350
|
+
# This is used to patch methods on native python types to handle
|
|
351
|
+
# the (unlikely) possibility of them getting called on a symbolic
|
|
352
|
+
# directly (e.g. `map(dict.pop, ...)`)
|
|
353
|
+
#
|
|
354
|
+
# Generally, we apply this patch when the method takes no arguments
|
|
355
|
+
# and has a meaningful return value.
|
|
356
|
+
native_method = getattr(pytype, method_name)
|
|
357
|
+
|
|
358
|
+
def with_checked_self(self, *a, **kw):
|
|
359
|
+
with NoTracing():
|
|
360
|
+
if hasattr(self, "__ch_pytype__"):
|
|
361
|
+
if python_type(self) is pytype:
|
|
362
|
+
bound_method = getattr(self, method_name)
|
|
363
|
+
with ResumedTracing():
|
|
364
|
+
return bound_method(*a, **kw)
|
|
365
|
+
return native_method(self, *a, **kw)
|
|
366
|
+
|
|
367
|
+
functools.update_wrapper(with_checked_self, native_method)
|
|
368
|
+
return with_checked_self
|
|
369
|
+
|
|
370
|
+
|
|
302
371
|
def with_symbolic_self(symbolic_cls: Type, fn: Callable):
|
|
303
372
|
def call_with_symbolic_self(self, *args, **kwargs):
|
|
304
373
|
with NoTracing():
|
|
@@ -308,7 +377,8 @@ def with_symbolic_self(symbolic_cls: Type, fn: Callable):
|
|
|
308
377
|
elif any(isinstance(a, CrossHairValue) for a in args) or (
|
|
309
378
|
kwargs and any(isinstance(a, CrossHairValue) for a in kwargs.values())
|
|
310
379
|
):
|
|
311
|
-
|
|
380
|
+
# NOTE: _ch_create_from_literal is suppoerted for very few types right now
|
|
381
|
+
self = symbolic_cls._ch_create_from_literal(self)
|
|
312
382
|
target_fn = getattr(symbolic_cls, fn.__name__)
|
|
313
383
|
else:
|
|
314
384
|
args = map(realize, args)
|
|
@@ -359,7 +429,7 @@ def choose_type(space: StateSpace, from_type: Type, varname: str) -> Optional[Ty
|
|
|
359
429
|
probability_true=probability_true,
|
|
360
430
|
):
|
|
361
431
|
return typ
|
|
362
|
-
raise
|
|
432
|
+
raise CrossHairInternal
|
|
363
433
|
|
|
364
434
|
|
|
365
435
|
def get_constructor_signature(cls: Type) -> Optional[inspect.Signature]:
|
|
@@ -369,24 +439,30 @@ def get_constructor_signature(cls: Type) -> Optional[inspect.Signature]:
|
|
|
369
439
|
sig = resolve_signature(cls)
|
|
370
440
|
if isinstance(sig, inspect.Signature):
|
|
371
441
|
return sig
|
|
442
|
+
|
|
443
|
+
applicable_sigs: List[Signature] = []
|
|
372
444
|
new_fn = cls.__new__
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
445
|
+
if new_fn is not object.__new__:
|
|
446
|
+
sig = resolve_signature(new_fn)
|
|
447
|
+
if not isinstance(sig, str):
|
|
448
|
+
applicable_sigs.append(sig)
|
|
449
|
+
init_fn = cls.__init__
|
|
450
|
+
if init_fn is not object.__init__:
|
|
451
|
+
sig = resolve_signature(init_fn)
|
|
452
|
+
if not isinstance(sig, str):
|
|
453
|
+
sig = sig.replace(
|
|
454
|
+
return_annotation=object
|
|
455
|
+
) # make return types compatible (& use __new__'s return)
|
|
456
|
+
applicable_sigs.append(sig)
|
|
457
|
+
if len(applicable_sigs) == 0:
|
|
458
|
+
return inspect.Signature([])
|
|
459
|
+
if len(applicable_sigs) == 2:
|
|
460
|
+
sig = dynamic_typing.intersect_signatures(*applicable_sigs)
|
|
461
|
+
else:
|
|
462
|
+
sig = applicable_sigs[0]
|
|
463
|
+
# strip first argument ("self" or "cls")
|
|
464
|
+
newparams = list(sig.parameters.values())[1:]
|
|
465
|
+
return sig.replace(parameters=newparams)
|
|
390
466
|
|
|
391
467
|
|
|
392
468
|
_TYPE_HINTS = IdKeyedDict()
|
|
@@ -425,7 +501,7 @@ def proxy_for_class(typ: Type, varname: str) -> object:
|
|
|
425
501
|
# postconditions can be invalidated when the class has invariants.
|
|
426
502
|
raise IgnoreAttempt
|
|
427
503
|
except Exception as e:
|
|
428
|
-
debug("Root-cause type construction traceback:",
|
|
504
|
+
debug("Root-cause type construction traceback:", ch_stack(currently_handling=e))
|
|
429
505
|
raise CrosshairUnsupported(
|
|
430
506
|
f"error constructing {typename} instance: {name_of_type(type(e))}: {e}",
|
|
431
507
|
) from e
|
|
@@ -440,24 +516,32 @@ def proxy_for_class(typ: Type, varname: str) -> object:
|
|
|
440
516
|
return f"{repr(typ)}({format_boundargs(realized_args)})"
|
|
441
517
|
|
|
442
518
|
reprer.reprs[obj] = regenerate_construction_string
|
|
443
|
-
|
|
444
|
-
debug("repr register lazy", hex(id(obj)), typename)
|
|
445
519
|
return obj
|
|
446
520
|
|
|
447
521
|
|
|
448
522
|
def register_patch(entity: Callable, patch_value: Callable):
|
|
449
523
|
if entity in _PATCH_REGISTRATIONS:
|
|
450
|
-
raise
|
|
524
|
+
raise CrossHairInternal(f"Doubly registered patch: {entity}")
|
|
451
525
|
_PATCH_REGISTRATIONS[entity] = patch_value
|
|
452
526
|
|
|
453
527
|
|
|
454
|
-
def
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
528
|
+
def _reset_all_registrations():
|
|
529
|
+
global _SIMPLE_PROXIES
|
|
530
|
+
_SIMPLE_PROXIES.clear()
|
|
531
|
+
global _PATCH_REGISTRATIONS
|
|
532
|
+
_PATCH_REGISTRATIONS.clear()
|
|
533
|
+
global _OPCODE_PATCHES
|
|
534
|
+
_OPCODE_PATCHES.clear()
|
|
535
|
+
clear_contract_registrations()
|
|
458
536
|
|
|
459
537
|
|
|
460
538
|
def register_opcode_patch(module: TracingModule) -> None:
|
|
539
|
+
if type(module) in map(type, _OPCODE_PATCHES):
|
|
540
|
+
raise CrossHairInternal(
|
|
541
|
+
f"Doubly registered opcode patch module type: {type(module)}"
|
|
542
|
+
)
|
|
543
|
+
check_opcode_support(module.opcodes_wanted)
|
|
544
|
+
|
|
461
545
|
_OPCODE_PATCHES.append(module)
|
|
462
546
|
|
|
463
547
|
|
|
@@ -473,15 +557,18 @@ class SymbolicFactory:
|
|
|
473
557
|
self.pytype: Any = pytype
|
|
474
558
|
self.varname = varname
|
|
475
559
|
|
|
560
|
+
def get_suffixed_varname(self, suffix: str):
|
|
561
|
+
return self.varname + suffix + self.space.uniq()
|
|
562
|
+
|
|
476
563
|
@overload
|
|
477
564
|
def __call__(
|
|
478
565
|
self, typ: Callable[..., _T], suffix: str = "", allow_subtypes: bool = True
|
|
479
|
-
) -> _T:
|
|
480
|
-
...
|
|
566
|
+
) -> _T: ...
|
|
481
567
|
|
|
482
568
|
@overload
|
|
483
|
-
def __call__(
|
|
484
|
-
|
|
569
|
+
def __call__(
|
|
570
|
+
self, typ: Any, suffix: str = "", allow_subtypes: bool = True
|
|
571
|
+
) -> Any: ...
|
|
485
572
|
|
|
486
573
|
def __call__(self, typ, suffix: str = "", allow_subtypes: bool = True):
|
|
487
574
|
"""
|
|
@@ -497,12 +584,12 @@ class SymbolicFactory:
|
|
|
497
584
|
"""
|
|
498
585
|
return proxy_for_type(
|
|
499
586
|
typ,
|
|
500
|
-
self.
|
|
587
|
+
self.get_suffixed_varname(suffix),
|
|
501
588
|
allow_subtypes=allow_subtypes,
|
|
502
589
|
)
|
|
503
590
|
|
|
504
591
|
|
|
505
|
-
_SIMPLE_PROXIES: MutableMapping[
|
|
592
|
+
_SIMPLE_PROXIES: MutableMapping[type, Callable] = {}
|
|
506
593
|
|
|
507
594
|
SymbolicCreationCallback = Union[
|
|
508
595
|
# Sadly Callable[] doesn't support variable arguments. Just enumerate:
|
|
@@ -528,7 +615,7 @@ def register_type(typ: Type, creator: SymbolicCreationCallback) -> None:
|
|
|
528
615
|
typ
|
|
529
616
|
), f'Only origin types may be registered, not "{typ}": try "{origin_of(typ)}" instead.'
|
|
530
617
|
if typ in _SIMPLE_PROXIES:
|
|
531
|
-
raise
|
|
618
|
+
raise CrossHairInternal(f'Duplicate type "{typ}" registered')
|
|
532
619
|
_SIMPLE_PROXIES[typ] = creator
|
|
533
620
|
|
|
534
621
|
|
|
@@ -566,8 +653,7 @@ def proxy_for_type(
|
|
|
566
653
|
typ: Callable[..., _T],
|
|
567
654
|
varname: str,
|
|
568
655
|
allow_subtypes: bool = False,
|
|
569
|
-
) -> _T:
|
|
570
|
-
...
|
|
656
|
+
) -> _T: ...
|
|
571
657
|
|
|
572
658
|
|
|
573
659
|
@overload
|
|
@@ -575,8 +661,7 @@ def proxy_for_type(
|
|
|
575
661
|
typ: Any,
|
|
576
662
|
varname: str,
|
|
577
663
|
allow_subtypes: bool = False,
|
|
578
|
-
) -> Any:
|
|
579
|
-
...
|
|
664
|
+
) -> Any: ...
|
|
580
665
|
|
|
581
666
|
|
|
582
667
|
def proxy_for_type(
|
|
@@ -589,6 +674,11 @@ def proxy_for_type(
|
|
|
589
674
|
typ = normalize_pytype(typ)
|
|
590
675
|
origin = origin_of(typ)
|
|
591
676
|
type_args = type_args_of(typ)
|
|
677
|
+
while isinstance(origin, TypeAliasTypes):
|
|
678
|
+
type_var_bindings = dict(zip(origin.__type_params__, type_args))
|
|
679
|
+
unified = dynamic_typing.realize(origin.__value__, type_var_bindings)
|
|
680
|
+
return proxy_for_type(unified, varname, allow_subtypes)
|
|
681
|
+
|
|
592
682
|
# special cases
|
|
593
683
|
if isinstance(typ, type) and issubclass(typ, enum.Enum):
|
|
594
684
|
enum_values = list(typ) # type:ignore
|
|
@@ -598,8 +688,10 @@ def proxy_for_type(
|
|
|
598
688
|
if space.smt_fork(desc="choose_enum_" + str(enum_value)):
|
|
599
689
|
return enum_value
|
|
600
690
|
return enum_values[-1]
|
|
601
|
-
|
|
602
|
-
|
|
691
|
+
if not _SIMPLE_PROXIES:
|
|
692
|
+
from crosshair.core_and_libs import _make_registrations
|
|
693
|
+
|
|
694
|
+
_make_registrations()
|
|
603
695
|
proxy_factory = _SIMPLE_PROXIES.get(origin)
|
|
604
696
|
if proxy_factory:
|
|
605
697
|
recursive_proxy_factory = SymbolicFactory(space, typ, varname)
|
|
@@ -618,7 +710,7 @@ _ARG_GENERATION_RENAMES: Dict[str, Callable] = {}
|
|
|
618
710
|
|
|
619
711
|
def gen_args(sig: inspect.Signature) -> inspect.BoundArguments:
|
|
620
712
|
if is_tracing():
|
|
621
|
-
raise
|
|
713
|
+
raise CrossHairInternal
|
|
622
714
|
args = sig.bind_partial()
|
|
623
715
|
space = context_statespace()
|
|
624
716
|
for param in sig.parameters.values():
|
|
@@ -717,7 +809,7 @@ class ConditionCheckable(Checkable):
|
|
|
717
809
|
"assuming preconditions: ",
|
|
718
810
|
",".join([p.expr_source for p in conditions.pre]),
|
|
719
811
|
)
|
|
720
|
-
options.deadline =
|
|
812
|
+
options.deadline = monotonic() + options.per_condition_timeout
|
|
721
813
|
|
|
722
814
|
with condition_parser(options.analysis_kind):
|
|
723
815
|
analysis = analyze_calltree(options, conditions)
|
|
@@ -765,6 +857,9 @@ class ClampedCheckable(Checkable):
|
|
|
765
857
|
self.cls_file = filename
|
|
766
858
|
self.cls_start_line = start_line
|
|
767
859
|
|
|
860
|
+
def __repr__(self) -> str:
|
|
861
|
+
return f"ClampedCheckable({self.checkable})"
|
|
862
|
+
|
|
768
863
|
def analyze(self) -> Iterable[AnalysisMessage]:
|
|
769
864
|
cls_file = self.cls_file
|
|
770
865
|
ret = []
|
|
@@ -803,7 +898,7 @@ def analyze_any(
|
|
|
803
898
|
elif inspect.ismodule(entity):
|
|
804
899
|
yield from analyze_module(cast(types.ModuleType, entity), options)
|
|
805
900
|
else:
|
|
806
|
-
raise
|
|
901
|
+
raise CrossHairInternal("Entity type not analyzable: " + str(type(entity)))
|
|
807
902
|
|
|
808
903
|
|
|
809
904
|
def analyze_module(
|
|
@@ -1045,6 +1140,47 @@ class CallTreeAnalysis:
|
|
|
1045
1140
|
num_confirmed_paths: int = 0
|
|
1046
1141
|
|
|
1047
1142
|
|
|
1143
|
+
class MessageGenerator:
|
|
1144
|
+
def __init__(self, fn: Callable):
|
|
1145
|
+
self.filename = ""
|
|
1146
|
+
self.start_lineno = 0
|
|
1147
|
+
if hasattr(fn, "__code__"):
|
|
1148
|
+
code_obj = fn.__code__
|
|
1149
|
+
self.filename = code_obj.co_filename
|
|
1150
|
+
self.start_lineno = code_obj.co_firstlineno
|
|
1151
|
+
_, _, lines = sourcelines(fn)
|
|
1152
|
+
self.end_lineno = self.start_lineno + len(lines)
|
|
1153
|
+
|
|
1154
|
+
def make(
|
|
1155
|
+
self,
|
|
1156
|
+
message_type: MessageType,
|
|
1157
|
+
detail: str,
|
|
1158
|
+
suggested_filename: Optional[str],
|
|
1159
|
+
suggested_lineno: int,
|
|
1160
|
+
tb: str,
|
|
1161
|
+
) -> AnalysisMessage:
|
|
1162
|
+
if (
|
|
1163
|
+
suggested_filename is not None
|
|
1164
|
+
and (os.path.abspath(suggested_filename) == os.path.abspath(self.filename))
|
|
1165
|
+
and (self.start_lineno <= suggested_lineno <= self.end_lineno)
|
|
1166
|
+
):
|
|
1167
|
+
return AnalysisMessage(
|
|
1168
|
+
message_type, detail, suggested_filename, suggested_lineno, 0, tb
|
|
1169
|
+
)
|
|
1170
|
+
else:
|
|
1171
|
+
exprline = "<unknown>"
|
|
1172
|
+
if suggested_filename is not None:
|
|
1173
|
+
lines = linecache.getlines(suggested_filename)
|
|
1174
|
+
try:
|
|
1175
|
+
exprline = lines[suggested_lineno - 1].strip()
|
|
1176
|
+
except IndexError:
|
|
1177
|
+
pass
|
|
1178
|
+
detail = f'"{exprline}" yields {detail}'
|
|
1179
|
+
return AnalysisMessage(
|
|
1180
|
+
message_type, detail, self.filename, self.start_lineno, 0, tb
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
|
|
1048
1184
|
def analyze_calltree(
|
|
1049
1185
|
options: AnalysisOptions, conditions: Conditions
|
|
1050
1186
|
) -> CallTreeAnalysis:
|
|
@@ -1068,9 +1204,9 @@ def analyze_calltree(
|
|
|
1068
1204
|
max_uninteresting_iterations = options.get_max_uninteresting_iterations()
|
|
1069
1205
|
patched = Patched()
|
|
1070
1206
|
# TODO clean up how encofrced conditions works here?
|
|
1071
|
-
with
|
|
1207
|
+
with patched:
|
|
1072
1208
|
for i in range(1, options.max_iterations + 1):
|
|
1073
|
-
start =
|
|
1209
|
+
start = monotonic()
|
|
1074
1210
|
if start > options.deadline:
|
|
1075
1211
|
debug("Exceeded condition timeout, stopping")
|
|
1076
1212
|
break
|
|
@@ -1107,6 +1243,25 @@ def analyze_calltree(
|
|
|
1107
1243
|
call_analysis.failing_precondition_reason
|
|
1108
1244
|
)
|
|
1109
1245
|
|
|
1246
|
+
except NotDeterministic:
|
|
1247
|
+
# TODO: Improve nondeterminism helpfulness
|
|
1248
|
+
tb = extract_tb(sys.exc_info()[2])
|
|
1249
|
+
frame_filename, frame_lineno = frame_summary_for_fn(
|
|
1250
|
+
conditions.src_fn, tb
|
|
1251
|
+
)
|
|
1252
|
+
msg_gen = MessageGenerator(conditions.src_fn)
|
|
1253
|
+
call_analysis = CallAnalysis(
|
|
1254
|
+
VerificationStatus.REFUTED,
|
|
1255
|
+
[
|
|
1256
|
+
msg_gen.make(
|
|
1257
|
+
MessageType.EXEC_ERR,
|
|
1258
|
+
"NotDeterministic: Found a different execution paths after making the same decisions",
|
|
1259
|
+
frame_filename,
|
|
1260
|
+
frame_lineno,
|
|
1261
|
+
traceback.format_exc(),
|
|
1262
|
+
)
|
|
1263
|
+
],
|
|
1264
|
+
)
|
|
1110
1265
|
except UnexploredPath:
|
|
1111
1266
|
call_analysis = CallAnalysis(VerificationStatus.UNKNOWN)
|
|
1112
1267
|
except IgnoreAttempt:
|
|
@@ -1183,7 +1338,7 @@ PathCompeltionCallback = Callable[
|
|
|
1183
1338
|
BoundArguments,
|
|
1184
1339
|
Any,
|
|
1185
1340
|
Optional[BaseException],
|
|
1186
|
-
Optional[
|
|
1341
|
+
Optional[StackSummary],
|
|
1187
1342
|
],
|
|
1188
1343
|
bool,
|
|
1189
1344
|
]
|
|
@@ -1199,12 +1354,12 @@ def explore_paths(
|
|
|
1199
1354
|
"""
|
|
1200
1355
|
Runs a path exploration for use cases beyond invariant checking.
|
|
1201
1356
|
"""
|
|
1202
|
-
condition_start =
|
|
1357
|
+
condition_start = monotonic()
|
|
1203
1358
|
breakout = False
|
|
1204
1359
|
max_uninteresting_iterations = options.get_max_uninteresting_iterations()
|
|
1205
1360
|
for i in range(1, options.max_iterations + 1):
|
|
1206
1361
|
debug("Iteration ", i)
|
|
1207
|
-
itr_start =
|
|
1362
|
+
itr_start = monotonic()
|
|
1208
1363
|
if itr_start > condition_start + options.per_condition_timeout:
|
|
1209
1364
|
debug(
|
|
1210
1365
|
"Stopping due to --per_condition_timeout=",
|
|
@@ -1225,7 +1380,7 @@ def explore_paths(
|
|
|
1225
1380
|
args = deepcopyext(pre_args, CopyMode.REGULAR, {})
|
|
1226
1381
|
ret: object = None
|
|
1227
1382
|
user_exc: Optional[BaseException] = None
|
|
1228
|
-
user_exc_stack: Optional[
|
|
1383
|
+
user_exc_stack: Optional[StackSummary] = None
|
|
1229
1384
|
with ExceptionFilter() as efilter, ResumedTracing():
|
|
1230
1385
|
ret = fn(args)
|
|
1231
1386
|
if efilter.user_exc:
|
|
@@ -1266,102 +1421,6 @@ def explore_paths(
|
|
|
1266
1421
|
break
|
|
1267
1422
|
|
|
1268
1423
|
|
|
1269
|
-
class UnEqual:
|
|
1270
|
-
pass
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
_UNEQUAL = UnEqual()
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
def deep_eq(old_val: object, new_val: object, visiting: Set[Tuple[int, int]]) -> bool:
|
|
1277
|
-
# TODO: test just about all of this
|
|
1278
|
-
if old_val is new_val:
|
|
1279
|
-
return True
|
|
1280
|
-
if type(old_val) != type(new_val):
|
|
1281
|
-
return False
|
|
1282
|
-
visit_key = (id(old_val), id(new_val))
|
|
1283
|
-
if visit_key in visiting:
|
|
1284
|
-
return True
|
|
1285
|
-
visiting.add(visit_key)
|
|
1286
|
-
try:
|
|
1287
|
-
with NoTracing():
|
|
1288
|
-
is_ch_value = isinstance(old_val, CrossHairValue)
|
|
1289
|
-
if is_ch_value:
|
|
1290
|
-
return old_val == new_val
|
|
1291
|
-
elif hasattr(old_val, "__dict__") and hasattr(new_val, "__dict__"):
|
|
1292
|
-
return deep_eq(old_val.__dict__, new_val.__dict__, visiting)
|
|
1293
|
-
elif isinstance(old_val, dict):
|
|
1294
|
-
assert isinstance(new_val, dict)
|
|
1295
|
-
for key in set(itertools.chain(old_val.keys(), *new_val.keys())):
|
|
1296
|
-
if (key in old_val) ^ (key in new_val):
|
|
1297
|
-
return False
|
|
1298
|
-
if not deep_eq(
|
|
1299
|
-
old_val.get(key, _UNEQUAL), new_val.get(key, _UNEQUAL), visiting
|
|
1300
|
-
):
|
|
1301
|
-
return False
|
|
1302
|
-
return True
|
|
1303
|
-
elif isinstance(old_val, Iterable):
|
|
1304
|
-
assert isinstance(new_val, Sized)
|
|
1305
|
-
if isinstance(old_val, Sized):
|
|
1306
|
-
if len(old_val) != len(new_val):
|
|
1307
|
-
return False
|
|
1308
|
-
assert isinstance(new_val, Iterable)
|
|
1309
|
-
return all(
|
|
1310
|
-
deep_eq(o, n, visiting)
|
|
1311
|
-
for (o, n) in itertools.zip_longest(
|
|
1312
|
-
old_val, new_val, fillvalue=_UNEQUAL
|
|
1313
|
-
)
|
|
1314
|
-
)
|
|
1315
|
-
elif type(old_val) is object:
|
|
1316
|
-
# Plain object instances are close enough to equal for our purposes
|
|
1317
|
-
return True
|
|
1318
|
-
else:
|
|
1319
|
-
# hopefully this is just ints, bools, etc
|
|
1320
|
-
return old_val == new_val
|
|
1321
|
-
finally:
|
|
1322
|
-
visiting.remove(visit_key)
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
class MessageGenerator:
|
|
1326
|
-
def __init__(self, fn: Callable):
|
|
1327
|
-
self.filename = ""
|
|
1328
|
-
if hasattr(fn, "__code__"):
|
|
1329
|
-
code_obj = fn.__code__
|
|
1330
|
-
self.filename = code_obj.co_filename
|
|
1331
|
-
self.start_lineno = code_obj.co_firstlineno
|
|
1332
|
-
_, _, lines = sourcelines(fn)
|
|
1333
|
-
self.end_lineno = self.start_lineno + len(lines)
|
|
1334
|
-
|
|
1335
|
-
def make(
|
|
1336
|
-
self,
|
|
1337
|
-
message_type: MessageType,
|
|
1338
|
-
detail: str,
|
|
1339
|
-
suggested_filename: Optional[str],
|
|
1340
|
-
suggested_lineno: int,
|
|
1341
|
-
tb: str,
|
|
1342
|
-
) -> AnalysisMessage:
|
|
1343
|
-
if (
|
|
1344
|
-
suggested_filename is not None
|
|
1345
|
-
and (os.path.abspath(suggested_filename) == os.path.abspath(self.filename))
|
|
1346
|
-
and (self.start_lineno <= suggested_lineno <= self.end_lineno)
|
|
1347
|
-
):
|
|
1348
|
-
return AnalysisMessage(
|
|
1349
|
-
message_type, detail, suggested_filename, suggested_lineno, 0, tb
|
|
1350
|
-
)
|
|
1351
|
-
else:
|
|
1352
|
-
exprline = "<unknown>"
|
|
1353
|
-
if suggested_filename is not None:
|
|
1354
|
-
lines = linecache.getlines(suggested_filename)
|
|
1355
|
-
try:
|
|
1356
|
-
exprline = lines[suggested_lineno - 1].strip()
|
|
1357
|
-
except IndexError:
|
|
1358
|
-
pass
|
|
1359
|
-
detail = f'"{exprline}" yields {detail}'
|
|
1360
|
-
return AnalysisMessage(
|
|
1361
|
-
message_type, detail, self.filename, self.start_lineno, 0, tb
|
|
1362
|
-
)
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
1424
|
def make_counterexample_message(
|
|
1366
1425
|
conditions: Conditions, args: BoundArguments, return_val: object = None
|
|
1367
1426
|
) -> str:
|
|
@@ -1457,11 +1516,11 @@ def attempt_call(
|
|
|
1457
1516
|
detail = name_of_type(type(e)) + ": " + str(e)
|
|
1458
1517
|
tb_desc = tb.format()
|
|
1459
1518
|
frame_filename, frame_lineno = frame_summary_for_fn(conditions.src_fn, tb)
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
debug("exception
|
|
1519
|
+
with ResumedTracing():
|
|
1520
|
+
space.detach_path(e)
|
|
1521
|
+
detail += " " + make_counterexample_message(conditions, original_args)
|
|
1522
|
+
debug("exception while evaluating function body:", detail)
|
|
1523
|
+
debug("exception traceback:", ch_stack(tb))
|
|
1465
1524
|
return CallAnalysis(
|
|
1466
1525
|
VerificationStatus.REFUTED,
|
|
1467
1526
|
[
|
|
@@ -1481,10 +1540,8 @@ def attempt_call(
|
|
|
1481
1540
|
and argname not in conditions.mutable_args
|
|
1482
1541
|
):
|
|
1483
1542
|
old_val, new_val = original_args.arguments[argname], argval
|
|
1484
|
-
# TODO: Do we really need custom equality here? Would love to drop that
|
|
1485
|
-
# `deep_eq` function.
|
|
1486
1543
|
with ResumedTracing():
|
|
1487
|
-
if
|
|
1544
|
+
if old_val != new_val:
|
|
1488
1545
|
space.detach_path()
|
|
1489
1546
|
detail = 'Argument "{}" is not marked as mutable, but changed from {} to {}'.format(
|
|
1490
1547
|
argname, old_val, new_val
|
|
@@ -1511,14 +1568,13 @@ def attempt_call(
|
|
|
1511
1568
|
elif efilter.user_exc is not None:
|
|
1512
1569
|
(e, tb) = efilter.user_exc
|
|
1513
1570
|
detail = name_of_type(type(e)) + ": " + str(e)
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
)
|
|
1571
|
+
with ResumedTracing():
|
|
1572
|
+
space.detach_path(e)
|
|
1573
|
+
detail += " " + make_counterexample_message(
|
|
1574
|
+
conditions, original_args, __return__
|
|
1575
|
+
)
|
|
1520
1576
|
debug("exception while calling postcondition:", detail)
|
|
1521
|
-
debug("exception traceback:",
|
|
1577
|
+
debug("exception traceback:", ch_stack(tb))
|
|
1522
1578
|
failures = [
|
|
1523
1579
|
msg_gen.make(
|
|
1524
1580
|
MessageType.POST_ERR,
|
|
@@ -1570,7 +1626,7 @@ def _mutability_testing_hash(o: object) -> int:
|
|
|
1570
1626
|
|
|
1571
1627
|
def is_deeply_immutable(o: object) -> bool:
|
|
1572
1628
|
if not is_tracing():
|
|
1573
|
-
raise
|
|
1629
|
+
raise CrossHairInternal("is_deeply_immutable must be run with tracing enabled")
|
|
1574
1630
|
orig_modules = COMPOSITE_TRACER.get_modules()
|
|
1575
1631
|
hash_intercept_module = PatchingModule({hash: _mutability_testing_hash})
|
|
1576
1632
|
for module in reversed(orig_modules):
|