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/diff_behavior.py
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
import dataclasses
|
|
3
3
|
import dis
|
|
4
|
+
import enum
|
|
4
5
|
import inspect
|
|
5
6
|
import sys
|
|
6
7
|
import time
|
|
7
|
-
from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
|
|
8
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
|
|
8
9
|
|
|
10
|
+
from crosshair import IgnoreAttempt
|
|
9
11
|
from crosshair.condition_parser import condition_parser
|
|
10
|
-
from crosshair.core import ExceptionFilter, Patched, deep_realize, gen_args
|
|
12
|
+
from crosshair.core import ExceptionFilter, Patched, deep_realize, gen_args
|
|
11
13
|
from crosshair.fnutil import FunctionInfo
|
|
12
14
|
from crosshair.options import AnalysisOptions
|
|
13
15
|
from crosshair.statespace import (
|
|
@@ -17,14 +19,22 @@ from crosshair.statespace import (
|
|
|
17
19
|
StateSpaceContext,
|
|
18
20
|
VerificationStatus,
|
|
19
21
|
)
|
|
22
|
+
from crosshair.test_util import flexible_equal
|
|
20
23
|
from crosshair.tracers import (
|
|
21
24
|
COMPOSITE_TRACER,
|
|
22
25
|
CoverageResult,
|
|
23
26
|
CoverageTracingModule,
|
|
24
27
|
NoTracing,
|
|
25
28
|
PushedModule,
|
|
29
|
+
ResumedTracing,
|
|
26
30
|
)
|
|
27
|
-
from crosshair.util import IgnoreAttempt, UnexploredPath, debug
|
|
31
|
+
from crosshair.util import CrosshairUnsupported, IgnoreAttempt, UnexploredPath, debug
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ExceptionEquivalenceType(enum.Enum):
|
|
35
|
+
ALL = "ALL"
|
|
36
|
+
SAME_TYPE = "SAME_TYPE"
|
|
37
|
+
TYPE_AND_MESSAGE = "TYPE_AND_MESSAGE"
|
|
28
38
|
|
|
29
39
|
|
|
30
40
|
@dataclasses.dataclass
|
|
@@ -65,16 +75,16 @@ class Result:
|
|
|
65
75
|
|
|
66
76
|
def describe_behavior(
|
|
67
77
|
fn: Callable, args: inspect.BoundArguments
|
|
68
|
-
) -> Tuple[
|
|
78
|
+
) -> Tuple[Any, Optional[BaseException]]:
|
|
69
79
|
with ExceptionFilter() as efilter:
|
|
70
80
|
ret = fn(*args.args, **args.kwargs)
|
|
71
81
|
return (ret, None)
|
|
72
82
|
if efilter.user_exc is not None:
|
|
73
83
|
exc = efilter.user_exc[0]
|
|
74
84
|
debug("user-level exception found", repr(exc), *efilter.user_exc[1])
|
|
75
|
-
return (None,
|
|
85
|
+
return (None, exc)
|
|
76
86
|
if efilter.ignore:
|
|
77
|
-
return (None,
|
|
87
|
+
return (None, IgnoreAttempt())
|
|
78
88
|
assert False
|
|
79
89
|
|
|
80
90
|
|
|
@@ -118,21 +128,30 @@ def diff_scorer(
|
|
|
118
128
|
|
|
119
129
|
|
|
120
130
|
def diff_behavior(
|
|
121
|
-
ctxfn1: FunctionInfo,
|
|
131
|
+
ctxfn1: FunctionInfo,
|
|
132
|
+
ctxfn2: FunctionInfo,
|
|
133
|
+
options: AnalysisOptions,
|
|
134
|
+
exception_equivalence: ExceptionEquivalenceType = ExceptionEquivalenceType.TYPE_AND_MESSAGE,
|
|
122
135
|
) -> Union[str, List[BehaviorDiff]]:
|
|
123
136
|
fn1, sig1 = ctxfn1.callable()
|
|
124
137
|
fn2, sig2 = ctxfn2.callable()
|
|
125
138
|
debug("Resolved signature:", sig1)
|
|
126
139
|
all_diffs: List[BehaviorDiff] = []
|
|
127
140
|
half1, half2 = options.split_limits(0.5)
|
|
128
|
-
with condition_parser(
|
|
141
|
+
with condition_parser(
|
|
142
|
+
options.analysis_kind
|
|
143
|
+
), Patched(), COMPOSITE_TRACER, NoTracing():
|
|
129
144
|
# We attempt both orderings of functions. This helps by:
|
|
130
145
|
# (1) avoiding code path explosions in one of the functions
|
|
131
146
|
# (2) using both signatures (in case they differ)
|
|
132
|
-
all_diffs.extend(
|
|
147
|
+
all_diffs.extend(
|
|
148
|
+
diff_behavior_with_signature(fn1, fn2, sig1, half1, exception_equivalence)
|
|
149
|
+
)
|
|
133
150
|
all_diffs.extend(
|
|
134
151
|
diff.reverse()
|
|
135
|
-
for diff in diff_behavior_with_signature(
|
|
152
|
+
for diff in diff_behavior_with_signature(
|
|
153
|
+
fn2, fn1, sig2, half2, exception_equivalence
|
|
154
|
+
)
|
|
136
155
|
)
|
|
137
156
|
debug("diff candidates:", all_diffs)
|
|
138
157
|
# greedily pick results:
|
|
@@ -156,7 +175,11 @@ def diff_behavior(
|
|
|
156
175
|
|
|
157
176
|
|
|
158
177
|
def diff_behavior_with_signature(
|
|
159
|
-
fn1: Callable,
|
|
178
|
+
fn1: Callable,
|
|
179
|
+
fn2: Callable,
|
|
180
|
+
sig: inspect.Signature,
|
|
181
|
+
options: AnalysisOptions,
|
|
182
|
+
exception_equivalence: ExceptionEquivalenceType,
|
|
160
183
|
) -> Iterable[BehaviorDiff]:
|
|
161
184
|
search_root = RootNode()
|
|
162
185
|
condition_start = time.monotonic()
|
|
@@ -180,7 +203,10 @@ def diff_behavior_with_signature(
|
|
|
180
203
|
with StateSpaceContext(space):
|
|
181
204
|
output = None
|
|
182
205
|
try:
|
|
183
|
-
|
|
206
|
+
with ResumedTracing():
|
|
207
|
+
(verification_status, output) = run_iteration(
|
|
208
|
+
fn1, fn2, sig, space, exception_equivalence
|
|
209
|
+
)
|
|
184
210
|
except IgnoreAttempt:
|
|
185
211
|
verification_status = None
|
|
186
212
|
except UnexploredPath:
|
|
@@ -209,8 +235,30 @@ def diff_behavior_with_signature(
|
|
|
209
235
|
break
|
|
210
236
|
|
|
211
237
|
|
|
238
|
+
def check_exception_equivalence(
|
|
239
|
+
exception_equivalence_type: ExceptionEquivalenceType,
|
|
240
|
+
exc1: Optional[BaseException],
|
|
241
|
+
exc2: Optional[BaseException],
|
|
242
|
+
) -> bool:
|
|
243
|
+
if exc1 is not None and exc2 is not None:
|
|
244
|
+
if exception_equivalence_type == ExceptionEquivalenceType.ALL:
|
|
245
|
+
return True
|
|
246
|
+
elif exception_equivalence_type == ExceptionEquivalenceType.SAME_TYPE:
|
|
247
|
+
return type(exc1) == type(exc2)
|
|
248
|
+
elif exception_equivalence_type == ExceptionEquivalenceType.TYPE_AND_MESSAGE:
|
|
249
|
+
return repr(exc1) == repr(exc2)
|
|
250
|
+
else:
|
|
251
|
+
raise CrosshairUnsupported("Invalid exception_equivalence type")
|
|
252
|
+
else:
|
|
253
|
+
return (exc1 is None) and (exc2 is None)
|
|
254
|
+
|
|
255
|
+
|
|
212
256
|
def run_iteration(
|
|
213
|
-
fn1: Callable,
|
|
257
|
+
fn1: Callable,
|
|
258
|
+
fn2: Callable,
|
|
259
|
+
sig: inspect.Signature,
|
|
260
|
+
space: StateSpace,
|
|
261
|
+
exception_equivalence: ExceptionEquivalenceType,
|
|
214
262
|
) -> Tuple[Optional[VerificationStatus], Optional[BehaviorDiff]]:
|
|
215
263
|
with NoTracing():
|
|
216
264
|
original_args = gen_args(sig)
|
|
@@ -220,12 +268,19 @@ def run_iteration(
|
|
|
220
268
|
with NoTracing():
|
|
221
269
|
coverage_manager = CoverageTracingModule(fn1, fn2)
|
|
222
270
|
with ExceptionFilter() as efilter, PushedModule(coverage_manager):
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
271
|
+
return1, exc1 = describe_behavior(fn1, args1)
|
|
272
|
+
return2, exc2 = describe_behavior(fn2, args2)
|
|
273
|
+
if (
|
|
274
|
+
flexible_equal(return1, return2)
|
|
275
|
+
and flexible_equal(args1.arguments, args2.arguments)
|
|
276
|
+
and check_exception_equivalence(exception_equivalence, exc1, exc2)
|
|
277
|
+
):
|
|
278
|
+
# Functions are equivalent if both have the same result,
|
|
279
|
+
# and deemed to have the same kind of error.
|
|
280
|
+
space.detach_path()
|
|
227
281
|
debug("Functions equivalent")
|
|
228
282
|
return (VerificationStatus.CONFIRMED, None)
|
|
283
|
+
space.detach_path()
|
|
229
284
|
debug("Functions differ")
|
|
230
285
|
realized_args = {
|
|
231
286
|
k: repr(deep_realize(v)) for (k, v) in original_args.arguments.items()
|
|
@@ -239,13 +294,13 @@ def run_iteration(
|
|
|
239
294
|
diff = BehaviorDiff(
|
|
240
295
|
realized_args,
|
|
241
296
|
Result(
|
|
242
|
-
repr(deep_realize(
|
|
243
|
-
|
|
297
|
+
repr(deep_realize(return1)),
|
|
298
|
+
repr(deep_realize(exc1)) if exc1 is not None else None,
|
|
244
299
|
post_execution_args1,
|
|
245
300
|
),
|
|
246
301
|
Result(
|
|
247
|
-
repr(deep_realize(
|
|
248
|
-
|
|
302
|
+
repr(deep_realize(return2)),
|
|
303
|
+
repr(deep_realize(exc2)) if exc2 is not None else None,
|
|
249
304
|
post_execution_args2,
|
|
250
305
|
),
|
|
251
306
|
coverage_manager.get_results(fn1),
|
crosshair/diff_behavior_test.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import unittest
|
|
3
1
|
from typing import Callable, List, Optional
|
|
4
2
|
|
|
5
|
-
from crosshair.diff_behavior import
|
|
3
|
+
from crosshair.diff_behavior import (
|
|
4
|
+
BehaviorDiff,
|
|
5
|
+
ExceptionEquivalenceType,
|
|
6
|
+
diff_behavior,
|
|
7
|
+
)
|
|
6
8
|
from crosshair.fnutil import FunctionInfo, walk_qualname
|
|
7
9
|
from crosshair.options import DEFAULT_OPTIONS
|
|
8
|
-
from crosshair.util import
|
|
10
|
+
from crosshair.util import debug, set_debug
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
def _foo1(x: int) -> int:
|
|
@@ -50,7 +52,34 @@ class Derived(Base):
|
|
|
50
52
|
return 11
|
|
51
53
|
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
def _sum_list_original(int_list):
|
|
56
|
+
count = 0
|
|
57
|
+
for i in int_list:
|
|
58
|
+
count += i
|
|
59
|
+
return count
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _sum_list_rewrite(int_list):
|
|
63
|
+
count = 0
|
|
64
|
+
for i in range(len(int_list)):
|
|
65
|
+
count += int_list[i]
|
|
66
|
+
return count
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _sum_list_rewrite_2(int_list):
|
|
70
|
+
class CustomException(Exception):
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
count = 0
|
|
75
|
+
for i in range(len(int_list)):
|
|
76
|
+
count += int_list[i]
|
|
77
|
+
except: # noqa E722
|
|
78
|
+
raise CustomException()
|
|
79
|
+
return count
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TestBehaviorDiff:
|
|
54
83
|
def test_diff_method(self):
|
|
55
84
|
diffs = diff_behavior(
|
|
56
85
|
walk_qualname(Base, "foo"),
|
|
@@ -58,10 +87,9 @@ class BehaviorDiffTest(unittest.TestCase):
|
|
|
58
87
|
DEFAULT_OPTIONS.overlay(max_iterations=10),
|
|
59
88
|
)
|
|
60
89
|
assert isinstance(diffs, list)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
90
|
+
assert [(d.result1.return_repr, d.result2.return_repr) for d in diffs] == [
|
|
91
|
+
("10", "11")
|
|
92
|
+
]
|
|
65
93
|
|
|
66
94
|
def test_diff_staticmethod(self):
|
|
67
95
|
diffs = diff_behavior(
|
|
@@ -69,20 +97,20 @@ class BehaviorDiffTest(unittest.TestCase):
|
|
|
69
97
|
foo2,
|
|
70
98
|
DEFAULT_OPTIONS.overlay(max_iterations=10),
|
|
71
99
|
)
|
|
72
|
-
|
|
100
|
+
assert diffs == []
|
|
73
101
|
|
|
74
102
|
def test_diff_behavior_same(self) -> None:
|
|
75
103
|
diffs = diff_behavior(foo1, foo2, DEFAULT_OPTIONS.overlay(max_iterations=10))
|
|
76
|
-
|
|
104
|
+
assert diffs == []
|
|
77
105
|
|
|
78
106
|
def test_diff_behavior_different(self) -> None:
|
|
79
107
|
diffs = diff_behavior(foo1, foo3, DEFAULT_OPTIONS.overlay(max_iterations=10))
|
|
80
|
-
|
|
108
|
+
assert len(diffs) == 1
|
|
81
109
|
diff = diffs[0]
|
|
82
110
|
assert isinstance(diff, BehaviorDiff)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
111
|
+
assert int(diff.args["x"]) > 1000
|
|
112
|
+
assert diff.result1.return_repr == "100"
|
|
113
|
+
assert diff.result2.return_repr == "1000"
|
|
86
114
|
|
|
87
115
|
def test_diff_behavior_mutation(self) -> None:
|
|
88
116
|
def cut_out_item1(a: List[int], i: int):
|
|
@@ -99,10 +127,10 @@ class BehaviorDiffTest(unittest.TestCase):
|
|
|
99
127
|
opts,
|
|
100
128
|
)
|
|
101
129
|
assert not isinstance(diffs, str)
|
|
102
|
-
|
|
130
|
+
assert len(diffs) == 1
|
|
103
131
|
diff = diffs[0]
|
|
104
|
-
|
|
105
|
-
|
|
132
|
+
assert len(diff.args["a"]) > 1
|
|
133
|
+
assert diff.args["i"] == "-1"
|
|
106
134
|
|
|
107
135
|
def test_example_coverage(self) -> None:
|
|
108
136
|
# Try to get examples that highlist the differences in the code.
|
|
@@ -128,7 +156,7 @@ class BehaviorDiffTest(unittest.TestCase):
|
|
|
128
156
|
debug("diffs=", diffs)
|
|
129
157
|
assert not isinstance(diffs, str)
|
|
130
158
|
return_vals = set((d.result1.return_repr, d.result2.return_repr) for d in diffs)
|
|
131
|
-
|
|
159
|
+
assert return_vals == {("False", "None"), ("False", "True")}
|
|
132
160
|
|
|
133
161
|
|
|
134
162
|
def test_diff_behavior_lambda() -> None:
|
|
@@ -146,7 +174,88 @@ def test_diff_behavior_lambda() -> None:
|
|
|
146
174
|
assert diffs == []
|
|
147
175
|
|
|
148
176
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
177
|
+
def test_diffbehavior_exceptions_default() -> None:
|
|
178
|
+
"""
|
|
179
|
+
Default behavior of `diffbehavior` - treating exceptions as different.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
diffs = diff_behavior(
|
|
183
|
+
FunctionInfo.from_fn(_sum_list_original),
|
|
184
|
+
FunctionInfo.from_fn(_sum_list_rewrite),
|
|
185
|
+
DEFAULT_OPTIONS,
|
|
186
|
+
)
|
|
187
|
+
debug("diffs=", diffs)
|
|
188
|
+
assert len(diffs) == 1 # finds a counter-example
|
|
189
|
+
assert isinstance(diffs[0], BehaviorDiff)
|
|
190
|
+
assert diffs[0].result1
|
|
191
|
+
assert isinstance(diffs[0].result1.error, str)
|
|
192
|
+
assert isinstance(diffs[0].result2.error, str)
|
|
193
|
+
assert diffs[0].result1.error.startswith("TypeError")
|
|
194
|
+
assert diffs[0].result2.error.startswith("TypeError")
|
|
195
|
+
assert (
|
|
196
|
+
diffs[0].result1.error != diffs[0].result2.error
|
|
197
|
+
) # Both code-blocks raise a different type error
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_diffbehavior_exceptions_same_type() -> None:
|
|
201
|
+
"""
|
|
202
|
+
Treat exceptions of the same type as equivalent.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
diffs = diff_behavior(
|
|
206
|
+
FunctionInfo.from_fn(_sum_list_original),
|
|
207
|
+
FunctionInfo.from_fn(_sum_list_rewrite),
|
|
208
|
+
DEFAULT_OPTIONS,
|
|
209
|
+
exception_equivalence=ExceptionEquivalenceType.SAME_TYPE,
|
|
210
|
+
)
|
|
211
|
+
debug("diffs=", diffs)
|
|
212
|
+
assert len(diffs) == 0 # No-counter example, because all TypeErrors are equal
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_diffbehavior_exceptions_all() -> None:
|
|
216
|
+
"""
|
|
217
|
+
Treat exceptions of all types as equivalent.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
diffs = diff_behavior(
|
|
221
|
+
FunctionInfo.from_fn(_sum_list_original),
|
|
222
|
+
FunctionInfo.from_fn(_sum_list_rewrite_2),
|
|
223
|
+
DEFAULT_OPTIONS,
|
|
224
|
+
exception_equivalence=ExceptionEquivalenceType.ALL,
|
|
225
|
+
)
|
|
226
|
+
debug("diffs=", diffs)
|
|
227
|
+
assert len(diffs) == 0 # No-counter example, because all TypeErrors are equal
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_diffbehavior_exceptions_same_type_different() -> None:
|
|
231
|
+
"""
|
|
232
|
+
Find a counter-example when raising different exception types.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
diffs = diff_behavior(
|
|
236
|
+
FunctionInfo.from_fn(_sum_list_original),
|
|
237
|
+
FunctionInfo.from_fn(_sum_list_rewrite_2),
|
|
238
|
+
DEFAULT_OPTIONS,
|
|
239
|
+
exception_equivalence=ExceptionEquivalenceType.SAME_TYPE,
|
|
240
|
+
)
|
|
241
|
+
debug("diffs=", diffs)
|
|
242
|
+
assert (
|
|
243
|
+
len(diffs) == 1
|
|
244
|
+
) # finds a counter-example, because TypeError!=CustomException
|
|
245
|
+
assert isinstance(diffs[0], BehaviorDiff)
|
|
246
|
+
assert isinstance(diffs[0].result1.error, str)
|
|
247
|
+
assert isinstance(diffs[0].result2.error, str)
|
|
248
|
+
assert diffs[0].result1.error.startswith("TypeError")
|
|
249
|
+
assert diffs[0].result2.error.startswith("CustomException")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_diff_behavior_nan() -> None:
|
|
253
|
+
def f(x: float):
|
|
254
|
+
return x
|
|
255
|
+
|
|
256
|
+
diffs = diff_behavior(
|
|
257
|
+
FunctionInfo.from_fn(f),
|
|
258
|
+
FunctionInfo.from_fn(f),
|
|
259
|
+
DEFAULT_OPTIONS,
|
|
260
|
+
)
|
|
261
|
+
assert diffs == []
|
crosshair/dynamic_typing.py
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import collections.abc
|
|
2
|
+
import sys
|
|
2
3
|
import typing
|
|
3
|
-
from
|
|
4
|
+
from inspect import Parameter, Signature
|
|
5
|
+
from itertools import zip_longest
|
|
6
|
+
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Type
|
|
4
7
|
|
|
5
8
|
import typing_inspect # type: ignore
|
|
6
9
|
|
|
10
|
+
from crosshair.util import debug # type: ignore
|
|
11
|
+
|
|
7
12
|
_EMPTYSET: frozenset = frozenset()
|
|
8
13
|
|
|
9
14
|
|
|
@@ -54,7 +59,7 @@ def unify_callable_args(
|
|
|
54
59
|
return True
|
|
55
60
|
if len(value_types) != len(recv_types):
|
|
56
61
|
return False
|
|
57
|
-
for
|
|
62
|
+
for varg, rarg in zip(value_types, recv_types):
|
|
58
63
|
# note reversal here: Callable is contravariant in argument types
|
|
59
64
|
if not unify(rarg, varg, bindings):
|
|
60
65
|
return False
|
|
@@ -201,7 +206,7 @@ def unify(
|
|
|
201
206
|
vargs = [object for _ in rargs]
|
|
202
207
|
else:
|
|
203
208
|
return False
|
|
204
|
-
for
|
|
209
|
+
for varg, targ in zip(vargs, rargs):
|
|
205
210
|
if not unify(varg, targ, bindings):
|
|
206
211
|
return False
|
|
207
212
|
return True
|
|
@@ -219,18 +224,123 @@ def get_bindings_from_type_arguments(pytype: Type) -> Mapping[object, type]:
|
|
|
219
224
|
return {}
|
|
220
225
|
|
|
221
226
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
newargs
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
pytype_origin
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
227
|
+
if sys.version_info >= (3, 9):
|
|
228
|
+
|
|
229
|
+
def realize(pytype: Type, bindings: Mapping[object, type]) -> object:
|
|
230
|
+
if typing_inspect.is_typevar(pytype):
|
|
231
|
+
return bindings[pytype]
|
|
232
|
+
if not hasattr(pytype, "__args__"):
|
|
233
|
+
return pytype
|
|
234
|
+
newargs: List = []
|
|
235
|
+
for arg in pytype.__args__: # type:ignore
|
|
236
|
+
newargs.append(realize(arg, bindings))
|
|
237
|
+
pytype_origin = origin_of(pytype)
|
|
238
|
+
if pytype_origin in (
|
|
239
|
+
collections.abc.Callable,
|
|
240
|
+
typing.Callable,
|
|
241
|
+
): # Callable args get flattened
|
|
242
|
+
newargs = [newargs[:-1], newargs[-1]]
|
|
243
|
+
return pytype_origin.__class_getitem__(tuple(newargs))
|
|
244
|
+
|
|
245
|
+
else:
|
|
246
|
+
|
|
247
|
+
def realize(pytype: Type, bindings: Mapping[object, type]) -> object:
|
|
248
|
+
if typing_inspect.is_typevar(pytype):
|
|
249
|
+
return bindings[pytype]
|
|
250
|
+
if not hasattr(pytype, "__args__"):
|
|
251
|
+
return pytype
|
|
252
|
+
newargs: List = []
|
|
253
|
+
for arg in pytype.__args__: # type:ignore
|
|
254
|
+
newargs.append(realize(arg, bindings))
|
|
255
|
+
# print('realizing pytype', repr(pytype), 'newargs', repr(newargs))
|
|
256
|
+
pytype_origin = origin_of(pytype)
|
|
257
|
+
if not hasattr(pytype_origin, "_name"):
|
|
258
|
+
pytype_origin = getattr(typing, pytype._name) # type:ignore
|
|
259
|
+
if pytype_origin is Callable: # Callable args get flattened
|
|
260
|
+
newargs = [newargs[:-1], newargs[-1]]
|
|
261
|
+
return pytype_origin.__getitem__(tuple(newargs))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def isolate_var_params(
|
|
265
|
+
sig: Signature,
|
|
266
|
+
) -> Tuple[
|
|
267
|
+
List[Parameter], Dict[str, Parameter], Optional[Parameter], Optional[Parameter]
|
|
268
|
+
]:
|
|
269
|
+
pos_only_params: List[Parameter] = []
|
|
270
|
+
keyword_params: Dict[str, Parameter] = {}
|
|
271
|
+
var_positional: Optional[Parameter] = None
|
|
272
|
+
var_keyword: Optional[Parameter] = None
|
|
273
|
+
for name, param in sig.parameters.items():
|
|
274
|
+
if param.kind == Parameter.VAR_POSITIONAL:
|
|
275
|
+
var_positional = param
|
|
276
|
+
elif param.kind == Parameter.VAR_KEYWORD:
|
|
277
|
+
var_keyword = param
|
|
278
|
+
elif param.kind == Parameter.POSITIONAL_ONLY:
|
|
279
|
+
pos_only_params.append(param)
|
|
280
|
+
else:
|
|
281
|
+
keyword_params[name] = param
|
|
282
|
+
return pos_only_params, keyword_params, var_positional, var_keyword
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def intersect_signatures(
|
|
286
|
+
sig1: Signature,
|
|
287
|
+
sig2: Signature,
|
|
288
|
+
) -> Signature:
|
|
289
|
+
"""
|
|
290
|
+
Approximate the intersection of two signatures.
|
|
291
|
+
The resulting signature may be overly loose
|
|
292
|
+
(matching some inputs that neither of the original signatures would match),
|
|
293
|
+
but it should cover all the inputs for each original signature.
|
|
294
|
+
|
|
295
|
+
One minor exception: All arguments that are allowed to be called as
|
|
296
|
+
keyword arguments will be converted to keyword-only arguments.
|
|
297
|
+
We do this to resolve the abiguity when position-or-keyword arguments
|
|
298
|
+
appear in the same position but with different names.
|
|
299
|
+
"""
|
|
300
|
+
pos1, key1, var_pos1, var_key1 = isolate_var_params(sig1)
|
|
301
|
+
pos2, key2, var_pos2, var_key2 = isolate_var_params(sig2)
|
|
302
|
+
is_squishy1 = var_pos1 is not None or var_key1 is not None
|
|
303
|
+
is_squishy2 = var_pos2 is not None or var_key2 is not None
|
|
304
|
+
out_params: Dict[str, Parameter] = {}
|
|
305
|
+
for p1, p2 in zip_longest(pos1, pos2):
|
|
306
|
+
if p1 is None:
|
|
307
|
+
if is_squishy1:
|
|
308
|
+
out_params[p2.name] = p2
|
|
309
|
+
elif p2 is None:
|
|
310
|
+
if is_squishy2:
|
|
311
|
+
out_params[p1.name] = p1
|
|
312
|
+
elif unify(p1.annotation, p2.annotation):
|
|
313
|
+
out_params[p1.name] = p1
|
|
314
|
+
else:
|
|
315
|
+
out_params[p2.name] = p2
|
|
316
|
+
for key in [
|
|
317
|
+
k
|
|
318
|
+
for pair in zip_longest(key1.keys(), key2.keys())
|
|
319
|
+
for k in pair
|
|
320
|
+
if k is not None
|
|
321
|
+
]:
|
|
322
|
+
if key not in key2:
|
|
323
|
+
if is_squishy2:
|
|
324
|
+
out_params[key] = key1[key].replace(kind=Parameter.KEYWORD_ONLY)
|
|
325
|
+
continue
|
|
326
|
+
if key not in key1:
|
|
327
|
+
if is_squishy1:
|
|
328
|
+
out_params[key] = key2[key].replace(kind=Parameter.KEYWORD_ONLY)
|
|
329
|
+
continue
|
|
330
|
+
if unify(key1[key].annotation, key2[key].annotation):
|
|
331
|
+
out_params[key] = key1[key].replace(kind=Parameter.KEYWORD_ONLY)
|
|
332
|
+
else:
|
|
333
|
+
out_params[key] = key2[key].replace(kind=Parameter.KEYWORD_ONLY)
|
|
334
|
+
if var_pos1 and var_pos2:
|
|
335
|
+
out_params[var_pos1.name] = var_pos1
|
|
336
|
+
if var_key1 and var_key2:
|
|
337
|
+
out_params[var_key1.name] = var_key1
|
|
338
|
+
if unify(sig1.return_annotation, sig2.return_annotation):
|
|
339
|
+
out_return_annotation = sig1.return_annotation
|
|
340
|
+
else:
|
|
341
|
+
out_return_annotation = sig2.return_annotation
|
|
342
|
+
result = Signature(
|
|
343
|
+
parameters=list(out_params.values()), return_annotation=out_return_annotation
|
|
344
|
+
)
|
|
345
|
+
debug("Combined __init__ and __new__ signatures", sig1, "and", sig2, "into", result)
|
|
346
|
+
return result
|