crosshair-tool 0.0.99__cp312-cp312-macosx_10_13_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- _crosshair_tracers.cpython-312-darwin.so +0 -0
- crosshair/__init__.py +42 -0
- crosshair/__main__.py +8 -0
- crosshair/_mark_stacks.h +790 -0
- crosshair/_preliminaries_test.py +18 -0
- crosshair/_tracers.h +94 -0
- crosshair/_tracers_pycompat.h +522 -0
- crosshair/_tracers_test.py +138 -0
- crosshair/abcstring.py +245 -0
- crosshair/auditwall.py +190 -0
- crosshair/auditwall_test.py +77 -0
- crosshair/codeconfig.py +113 -0
- crosshair/codeconfig_test.py +117 -0
- crosshair/condition_parser.py +1237 -0
- crosshair/condition_parser_test.py +497 -0
- crosshair/conftest.py +30 -0
- crosshair/copyext.py +155 -0
- crosshair/copyext_test.py +84 -0
- crosshair/core.py +1763 -0
- crosshair/core_and_libs.py +149 -0
- crosshair/core_regestered_types_test.py +82 -0
- crosshair/core_test.py +1316 -0
- crosshair/diff_behavior.py +314 -0
- crosshair/diff_behavior_test.py +261 -0
- crosshair/dynamic_typing.py +346 -0
- crosshair/dynamic_typing_test.py +210 -0
- crosshair/enforce.py +282 -0
- crosshair/enforce_test.py +182 -0
- crosshair/examples/PEP316/__init__.py +1 -0
- crosshair/examples/PEP316/bugs_detected/__init__.py +0 -0
- crosshair/examples/PEP316/bugs_detected/getattr_magic.py +16 -0
- crosshair/examples/PEP316/bugs_detected/hash_consistent_with_equals.py +31 -0
- crosshair/examples/PEP316/bugs_detected/shopping_cart.py +24 -0
- crosshair/examples/PEP316/bugs_detected/showcase.py +39 -0
- crosshair/examples/PEP316/correct_code/__init__.py +0 -0
- crosshair/examples/PEP316/correct_code/arith.py +60 -0
- crosshair/examples/PEP316/correct_code/chess.py +77 -0
- crosshair/examples/PEP316/correct_code/nesting_inference.py +17 -0
- crosshair/examples/PEP316/correct_code/numpy_examples.py +132 -0
- crosshair/examples/PEP316/correct_code/rolling_average.py +35 -0
- crosshair/examples/PEP316/correct_code/showcase.py +104 -0
- crosshair/examples/__init__.py +0 -0
- crosshair/examples/check_examples_test.py +146 -0
- crosshair/examples/deal/__init__.py +1 -0
- crosshair/examples/icontract/__init__.py +1 -0
- crosshair/examples/icontract/bugs_detected/__init__.py +0 -0
- crosshair/examples/icontract/bugs_detected/showcase.py +41 -0
- crosshair/examples/icontract/bugs_detected/wrong_sign.py +8 -0
- crosshair/examples/icontract/correct_code/__init__.py +0 -0
- crosshair/examples/icontract/correct_code/arith.py +51 -0
- crosshair/examples/icontract/correct_code/showcase.py +94 -0
- crosshair/fnutil.py +391 -0
- crosshair/fnutil_test.py +75 -0
- crosshair/fuzz_core_test.py +516 -0
- crosshair/libimpl/__init__.py +0 -0
- crosshair/libimpl/arraylib.py +161 -0
- crosshair/libimpl/binascii_ch_test.py +30 -0
- crosshair/libimpl/binascii_test.py +67 -0
- crosshair/libimpl/binasciilib.py +150 -0
- crosshair/libimpl/bisectlib_test.py +23 -0
- crosshair/libimpl/builtinslib.py +5228 -0
- crosshair/libimpl/builtinslib_ch_test.py +1191 -0
- crosshair/libimpl/builtinslib_test.py +3735 -0
- crosshair/libimpl/codecslib.py +86 -0
- crosshair/libimpl/codecslib_test.py +86 -0
- crosshair/libimpl/collectionslib.py +264 -0
- crosshair/libimpl/collectionslib_ch_test.py +252 -0
- crosshair/libimpl/collectionslib_test.py +332 -0
- crosshair/libimpl/copylib.py +23 -0
- crosshair/libimpl/copylib_test.py +18 -0
- crosshair/libimpl/datetimelib.py +2559 -0
- crosshair/libimpl/datetimelib_ch_test.py +354 -0
- crosshair/libimpl/datetimelib_test.py +112 -0
- crosshair/libimpl/decimallib.py +5257 -0
- crosshair/libimpl/decimallib_ch_test.py +78 -0
- crosshair/libimpl/decimallib_test.py +76 -0
- crosshair/libimpl/encodings/__init__.py +23 -0
- crosshair/libimpl/encodings/_encutil.py +187 -0
- crosshair/libimpl/encodings/ascii.py +44 -0
- crosshair/libimpl/encodings/latin_1.py +40 -0
- crosshair/libimpl/encodings/utf_8.py +93 -0
- crosshair/libimpl/encodings_ch_test.py +83 -0
- crosshair/libimpl/fractionlib.py +16 -0
- crosshair/libimpl/fractionlib_test.py +80 -0
- crosshair/libimpl/functoolslib.py +34 -0
- crosshair/libimpl/functoolslib_test.py +56 -0
- crosshair/libimpl/hashliblib.py +30 -0
- crosshair/libimpl/hashliblib_test.py +18 -0
- crosshair/libimpl/heapqlib.py +47 -0
- crosshair/libimpl/heapqlib_test.py +21 -0
- crosshair/libimpl/importliblib.py +18 -0
- crosshair/libimpl/importliblib_test.py +38 -0
- crosshair/libimpl/iolib.py +216 -0
- crosshair/libimpl/iolib_ch_test.py +128 -0
- crosshair/libimpl/iolib_test.py +19 -0
- crosshair/libimpl/ipaddresslib.py +8 -0
- crosshair/libimpl/itertoolslib.py +44 -0
- crosshair/libimpl/itertoolslib_test.py +44 -0
- crosshair/libimpl/jsonlib.py +984 -0
- crosshair/libimpl/jsonlib_ch_test.py +42 -0
- crosshair/libimpl/jsonlib_test.py +51 -0
- crosshair/libimpl/mathlib.py +179 -0
- crosshair/libimpl/mathlib_ch_test.py +44 -0
- crosshair/libimpl/mathlib_test.py +67 -0
- crosshair/libimpl/oslib.py +7 -0
- crosshair/libimpl/pathliblib_test.py +10 -0
- crosshair/libimpl/randomlib.py +178 -0
- crosshair/libimpl/randomlib_test.py +120 -0
- crosshair/libimpl/relib.py +846 -0
- crosshair/libimpl/relib_ch_test.py +169 -0
- crosshair/libimpl/relib_test.py +493 -0
- crosshair/libimpl/timelib.py +72 -0
- crosshair/libimpl/timelib_test.py +82 -0
- crosshair/libimpl/typeslib.py +15 -0
- crosshair/libimpl/typeslib_test.py +36 -0
- crosshair/libimpl/unicodedatalib.py +75 -0
- crosshair/libimpl/unicodedatalib_test.py +42 -0
- crosshair/libimpl/urlliblib.py +23 -0
- crosshair/libimpl/urlliblib_test.py +19 -0
- crosshair/libimpl/weakreflib.py +13 -0
- crosshair/libimpl/weakreflib_test.py +69 -0
- crosshair/libimpl/zliblib.py +15 -0
- crosshair/libimpl/zliblib_test.py +13 -0
- crosshair/lsp_server.py +261 -0
- crosshair/lsp_server_test.py +30 -0
- crosshair/main.py +973 -0
- crosshair/main_test.py +543 -0
- crosshair/objectproxy.py +376 -0
- crosshair/objectproxy_test.py +41 -0
- crosshair/opcode_intercept.py +601 -0
- crosshair/opcode_intercept_test.py +304 -0
- crosshair/options.py +218 -0
- crosshair/options_test.py +10 -0
- crosshair/patch_equivalence_test.py +75 -0
- crosshair/path_cover.py +209 -0
- crosshair/path_cover_test.py +138 -0
- crosshair/path_search.py +161 -0
- crosshair/path_search_test.py +52 -0
- crosshair/pathing_oracle.py +271 -0
- crosshair/pathing_oracle_test.py +21 -0
- crosshair/pure_importer.py +27 -0
- crosshair/pure_importer_test.py +16 -0
- crosshair/py.typed +0 -0
- crosshair/register_contract.py +273 -0
- crosshair/register_contract_test.py +190 -0
- crosshair/simplestructs.py +1165 -0
- crosshair/simplestructs_test.py +283 -0
- crosshair/smtlib.py +24 -0
- crosshair/smtlib_test.py +14 -0
- crosshair/statespace.py +1199 -0
- crosshair/statespace_test.py +108 -0
- crosshair/stubs_parser.py +352 -0
- crosshair/stubs_parser_test.py +43 -0
- crosshair/test_util.py +329 -0
- crosshair/test_util_test.py +26 -0
- crosshair/tools/__init__.py +0 -0
- crosshair/tools/check_help_in_doc.py +264 -0
- crosshair/tools/check_init_and_setup_coincide.py +119 -0
- crosshair/tools/generate_demo_table.py +127 -0
- crosshair/tracers.py +544 -0
- crosshair/tracers_test.py +154 -0
- crosshair/type_repo.py +151 -0
- crosshair/unicode_categories.py +589 -0
- crosshair/unicode_categories_test.py +27 -0
- crosshair/util.py +741 -0
- crosshair/util_test.py +173 -0
- crosshair/watcher.py +307 -0
- crosshair/watcher_test.py +107 -0
- crosshair/z3util.py +76 -0
- crosshair/z3util_test.py +11 -0
- crosshair_tool-0.0.99.dist-info/METADATA +144 -0
- crosshair_tool-0.0.99.dist-info/RECORD +176 -0
- crosshair_tool-0.0.99.dist-info/WHEEL +6 -0
- crosshair_tool-0.0.99.dist-info/entry_points.txt +3 -0
- crosshair_tool-0.0.99.dist-info/licenses/LICENSE +93 -0
- crosshair_tool-0.0.99.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import sys
|
|
3
|
+
from abc import ABCMeta
|
|
4
|
+
from typing import List, Set
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from crosshair.core_and_libs import NoTracing, proxy_for_type, standalone_statespace
|
|
9
|
+
from crosshair.libimpl.builtinslib import (
|
|
10
|
+
ModelingDirector,
|
|
11
|
+
RealBasedSymbolicFloat,
|
|
12
|
+
SymbolicBool,
|
|
13
|
+
SymbolicInt,
|
|
14
|
+
SymbolicType,
|
|
15
|
+
)
|
|
16
|
+
from crosshair.statespace import POST_FAIL
|
|
17
|
+
from crosshair.test_util import check_states
|
|
18
|
+
from crosshair.tracers import ResumedTracing
|
|
19
|
+
from crosshair.z3util import z3And
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_dict_index():
|
|
23
|
+
a = {"two": 2, "four": 4, "six": 6}
|
|
24
|
+
|
|
25
|
+
def numstr(x: str) -> int:
|
|
26
|
+
"""
|
|
27
|
+
post: _ != 4
|
|
28
|
+
raises: KeyError
|
|
29
|
+
"""
|
|
30
|
+
return a[x]
|
|
31
|
+
|
|
32
|
+
check_states(numstr, POST_FAIL)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_dict_index_without_realization(space):
|
|
36
|
+
class WithMeta(metaclass=ABCMeta):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
space.extra(ModelingDirector).global_representations[float] = RealBasedSymbolicFloat
|
|
40
|
+
a = {
|
|
41
|
+
-1: WithMeta,
|
|
42
|
+
# ^ tests regression: isinstance(WithMeta(), type) but type(WithMeta) != type
|
|
43
|
+
0: list,
|
|
44
|
+
1.0: 10.0,
|
|
45
|
+
2: 20,
|
|
46
|
+
3: 30,
|
|
47
|
+
4: 40,
|
|
48
|
+
("complex", "key"): 50,
|
|
49
|
+
6: math.inf,
|
|
50
|
+
7: math.inf,
|
|
51
|
+
}
|
|
52
|
+
int_key = proxy_for_type(int, "int_key")
|
|
53
|
+
int_key2 = proxy_for_type(int, "int_key2")
|
|
54
|
+
int_key3 = proxy_for_type(int, "int_key3")
|
|
55
|
+
float_key = RealBasedSymbolicFloat("float_key")
|
|
56
|
+
float_key2 = RealBasedSymbolicFloat("float_key2")
|
|
57
|
+
with ResumedTracing():
|
|
58
|
+
# Try some concrete values out first:
|
|
59
|
+
assert a[("complex", "key")] == 50
|
|
60
|
+
assert a[6] == float("inf")
|
|
61
|
+
try:
|
|
62
|
+
a[42]
|
|
63
|
+
assert False, "Expected KeyError for missing key 42"
|
|
64
|
+
except KeyError:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
space.add(2 <= int_key)
|
|
68
|
+
space.add(int_key <= 4)
|
|
69
|
+
int_result = a[int_key]
|
|
70
|
+
assert space.is_possible(int_result == 20)
|
|
71
|
+
assert space.is_possible(int_result == 40)
|
|
72
|
+
assert not space.is_possible(int_result == 10)
|
|
73
|
+
space.add(float_key == 1.0)
|
|
74
|
+
float_result = a[float_key]
|
|
75
|
+
assert space.is_possible(float_result == 10.0)
|
|
76
|
+
assert not space.is_possible(float_result == 42.0)
|
|
77
|
+
space.add(float_key2 == 2.0)
|
|
78
|
+
float_result2 = a[float_key2]
|
|
79
|
+
assert space.is_possible(float_result2 == 20)
|
|
80
|
+
space.add(int_key2 == 0)
|
|
81
|
+
int_result2 = a[int_key2]
|
|
82
|
+
assert int_result2 == list
|
|
83
|
+
space.add(any([int_key3 == 6, int_key3 == 7]))
|
|
84
|
+
inf_result = a[int_key3]
|
|
85
|
+
assert inf_result is math.inf
|
|
86
|
+
assert isinstance(int_result, SymbolicInt)
|
|
87
|
+
assert isinstance(float_result, RealBasedSymbolicFloat)
|
|
88
|
+
assert isinstance(float_result2, SymbolicInt)
|
|
89
|
+
assert isinstance(int_result2, SymbolicType)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_dict_symbolic_index_miss(space):
|
|
93
|
+
a = {6: 60, 7: 70}
|
|
94
|
+
x = proxy_for_type(int, "x")
|
|
95
|
+
with ResumedTracing():
|
|
96
|
+
space.add(x <= 4)
|
|
97
|
+
with pytest.raises(KeyError):
|
|
98
|
+
result = a[x]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_concrete_list_with_symbolic_index_simple(space):
|
|
102
|
+
haystack = [False] * 13 + [True] + [False] * 11
|
|
103
|
+
|
|
104
|
+
idx = proxy_for_type(int, "idx")
|
|
105
|
+
with ResumedTracing():
|
|
106
|
+
space.add(0 <= idx)
|
|
107
|
+
space.add(idx < len(haystack))
|
|
108
|
+
ret = haystack[idx]
|
|
109
|
+
assert isinstance(ret, SymbolicBool)
|
|
110
|
+
with ResumedTracing():
|
|
111
|
+
assert space.is_possible(idx == 13)
|
|
112
|
+
assert space.is_possible(idx == 12)
|
|
113
|
+
space.add(ret)
|
|
114
|
+
assert not space.is_possible(idx == 12)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_concrete_list_with_symbolic_index_unhashable_values(space):
|
|
118
|
+
o1 = dict()
|
|
119
|
+
options = [o1, o1, o1]
|
|
120
|
+
idx = proxy_for_type(int, "idx")
|
|
121
|
+
with ResumedTracing():
|
|
122
|
+
space.add(0 <= idx)
|
|
123
|
+
space.add(idx < 3)
|
|
124
|
+
ret = options[idx]
|
|
125
|
+
assert ret is o1
|
|
126
|
+
assert space.is_possible(idx == 0)
|
|
127
|
+
assert space.is_possible(idx == 2)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_dict_key_containment():
|
|
131
|
+
abc = {"two": 2, "four": 4, "six": 6}
|
|
132
|
+
|
|
133
|
+
def numstr(x: str) -> bool:
|
|
134
|
+
"""
|
|
135
|
+
post: _
|
|
136
|
+
"""
|
|
137
|
+
return x not in abc
|
|
138
|
+
|
|
139
|
+
check_states(numstr, POST_FAIL)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_dict_comprehension_basic():
|
|
143
|
+
with standalone_statespace as space:
|
|
144
|
+
with NoTracing():
|
|
145
|
+
x = proxy_for_type(int, "x")
|
|
146
|
+
space.add(x >= 40)
|
|
147
|
+
space.add(x < 50)
|
|
148
|
+
d = {k: v for k, v in ((35, 3), (x, 4))}
|
|
149
|
+
with NoTracing():
|
|
150
|
+
assert type(d) is not dict
|
|
151
|
+
for k in d:
|
|
152
|
+
if k == 35:
|
|
153
|
+
continue
|
|
154
|
+
with NoTracing():
|
|
155
|
+
assert type(k) is not int
|
|
156
|
+
assert space.is_possible(k == 43)
|
|
157
|
+
assert space.is_possible(k == 48)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_dict_comprehension_traces_during_custom_hash():
|
|
161
|
+
class FancyCompare:
|
|
162
|
+
def __init__(self, mystr: str):
|
|
163
|
+
self.mystr = mystr
|
|
164
|
+
|
|
165
|
+
def __eq__(self, other):
|
|
166
|
+
return (
|
|
167
|
+
isinstance(other, FancyCompare)
|
|
168
|
+
and "".join([self.mystr, ""]) == other.mystr
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def __hash__(self):
|
|
172
|
+
return hash(self.mystr)
|
|
173
|
+
|
|
174
|
+
with standalone_statespace as space:
|
|
175
|
+
with NoTracing():
|
|
176
|
+
mystr = proxy_for_type(str, "mystr")
|
|
177
|
+
# NOTE: If tracing isn't on when we call FancyCompare.__eq__, we'll get an
|
|
178
|
+
# exception here:
|
|
179
|
+
d = {x: 42 for x in [FancyCompare(mystr), FancyCompare(mystr)]}
|
|
180
|
+
# There is only one item:
|
|
181
|
+
assert len(d) == 1
|
|
182
|
+
# TODO: In theory, we shouldn't need to realize the string here (but we are):
|
|
183
|
+
# assert space.is_possible(mystr.__len__() == 0)
|
|
184
|
+
# assert space.is_possible(mystr.__len__() == 1)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_dict_comprehension_e2e():
|
|
188
|
+
def f(ls: List[int]) -> dict:
|
|
189
|
+
"""
|
|
190
|
+
post: 4321 not in __return__
|
|
191
|
+
"""
|
|
192
|
+
return {i: i for i in ls}
|
|
193
|
+
|
|
194
|
+
check_states(f, POST_FAIL)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@pytest.mark.skipif(
|
|
198
|
+
sys.version_info >= (3, 13), reason="Negation opcode changed; TODO: fix!"
|
|
199
|
+
)
|
|
200
|
+
def test_not_operator_on_bool():
|
|
201
|
+
with standalone_statespace as space:
|
|
202
|
+
with NoTracing():
|
|
203
|
+
boolval = proxy_for_type(bool, "boolval")
|
|
204
|
+
inverseval = not boolval
|
|
205
|
+
assert space.is_possible(inverseval)
|
|
206
|
+
with NoTracing():
|
|
207
|
+
assert type(inverseval) is not bool
|
|
208
|
+
assert not space.is_possible(z3And(boolval.var, inverseval.var))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_not_operator_on_non_bool():
|
|
212
|
+
with standalone_statespace as space:
|
|
213
|
+
with NoTracing():
|
|
214
|
+
intlist = proxy_for_type(List[int], "intlist")
|
|
215
|
+
space.add(intlist.__len__() == 0)
|
|
216
|
+
notList = not intlist
|
|
217
|
+
with NoTracing():
|
|
218
|
+
assert notList
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_set_comprehension_basic():
|
|
222
|
+
with standalone_statespace as space:
|
|
223
|
+
with NoTracing():
|
|
224
|
+
x = proxy_for_type(int, "x")
|
|
225
|
+
space.add(x >= 40)
|
|
226
|
+
space.add(x < 50)
|
|
227
|
+
result_set = {k for k in (35, x)}
|
|
228
|
+
with NoTracing():
|
|
229
|
+
assert type(result_set) is not set
|
|
230
|
+
for k in result_set:
|
|
231
|
+
if k == 35:
|
|
232
|
+
continue
|
|
233
|
+
with NoTracing():
|
|
234
|
+
assert type(k) is not int
|
|
235
|
+
assert space.is_possible(k == 43)
|
|
236
|
+
assert space.is_possible(k == 48)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_set_comprehension_e2e():
|
|
240
|
+
def f(s: Set[int]) -> Set:
|
|
241
|
+
"""
|
|
242
|
+
post: 4321 not in __return__
|
|
243
|
+
"""
|
|
244
|
+
return {i for i in s}
|
|
245
|
+
|
|
246
|
+
check_states(f, POST_FAIL)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_trace_disabling_at_jump_targets(space):
|
|
250
|
+
# This replicates a corruption of the interpreter stack in 3.12
|
|
251
|
+
# under a specific bytecode layout.
|
|
252
|
+
#
|
|
253
|
+
# The origial issue was caused by neglecting to keep sys.monitor probes
|
|
254
|
+
# alive (for post-op callbacks) that could be jumped to from other
|
|
255
|
+
# locations.
|
|
256
|
+
_global_type_lookupx = {
|
|
257
|
+
1: 1,
|
|
258
|
+
bool: 2,
|
|
259
|
+
3: 3,
|
|
260
|
+
}
|
|
261
|
+
with ResumedTracing():
|
|
262
|
+
_ = {
|
|
263
|
+
k: v
|
|
264
|
+
for k, v in _global_type_lookupx.items() # <- a new line has to be here (yes, the generated bytecode differs!)
|
|
265
|
+
if k == bool # The iteration filter needs to alternate
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# TODO: we could implement identity comparisons on 3.8 by intercepting COMPARE_OP
|
|
270
|
+
@pytest.mark.skipif(sys.version_info < (3, 9), reason="IS_OP is new in Python 3.9")
|
|
271
|
+
def test_identity_operator_on_booleans():
|
|
272
|
+
with standalone_statespace as space:
|
|
273
|
+
with NoTracing():
|
|
274
|
+
b1 = proxy_for_type(bool, "b1")
|
|
275
|
+
space.add(b1)
|
|
276
|
+
assert b1 is True
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@pytest.mark.skipif(sys.version_info < (3, 9), reason="IS_OP is new in Python 3.9")
|
|
280
|
+
def test_identity_operator_does_not_realize_on_differing_types():
|
|
281
|
+
with standalone_statespace as space:
|
|
282
|
+
with NoTracing():
|
|
283
|
+
b1 = proxy_for_type(bool, "b1")
|
|
284
|
+
choices_made_at_start = len(space.choices_made)
|
|
285
|
+
space.add(b1)
|
|
286
|
+
fourty_two = 42 # assignment just to avoid lint errors
|
|
287
|
+
b1 is fourty_two
|
|
288
|
+
assert len(space.choices_made) == choices_made_at_start
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class IExplodeOnRepr:
|
|
292
|
+
def __repr__(self):
|
|
293
|
+
raise ValueError("boom")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_postop_callback_skipped_on_exception_handler_jump(space):
|
|
297
|
+
with ResumedTracing():
|
|
298
|
+
elements = IExplodeOnRepr()
|
|
299
|
+
try:
|
|
300
|
+
ret = f"these are them: {elements!r}"
|
|
301
|
+
except ValueError: # pragma: no cover
|
|
302
|
+
ret = None
|
|
303
|
+
# need to do something(anything) with elements so that it's on the stack:
|
|
304
|
+
type(elements)
|
crosshair/options.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
import enum
|
|
3
|
+
import math
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import dataclass, replace
|
|
7
|
+
from typing import Any, Callable, Mapping, Optional, Sequence, Tuple, get_type_hints
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AnalysisKind(enum.Enum):
|
|
11
|
+
asserts = "asserts"
|
|
12
|
+
PEP316 = "PEP316"
|
|
13
|
+
icontract = "icontract"
|
|
14
|
+
deal = "deal"
|
|
15
|
+
|
|
16
|
+
def __repr__(self):
|
|
17
|
+
return f"AnalysisKind.{self.name}"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _parse_analysis_kind(argstr: str) -> Sequence[AnalysisKind]:
|
|
21
|
+
try:
|
|
22
|
+
return [AnalysisKind[part.strip()] for part in argstr.split(",")]
|
|
23
|
+
except KeyError:
|
|
24
|
+
raise ValueError
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _parse_bool(argstr: str) -> Optional[bool]:
|
|
28
|
+
match = re.fullmatch(r"(1|true|y(?:es)?)|(0|false|no?)", argstr, re.I)
|
|
29
|
+
if match:
|
|
30
|
+
yes, _no = match.groups()
|
|
31
|
+
return bool(yes)
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class AnalysisOptionSet:
|
|
37
|
+
"""
|
|
38
|
+
Encodes some set of partially-specified options.
|
|
39
|
+
|
|
40
|
+
This class is used while parsing options from various places.
|
|
41
|
+
It is very similar to `AnalysisOptions` (which is used during execution) but allows
|
|
42
|
+
None values everywhere so that options can correctly override each other.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
analysis_kind: Optional[Sequence[AnalysisKind]] = None
|
|
46
|
+
enabled: Optional[bool] = None
|
|
47
|
+
specs_complete: Optional[bool] = None
|
|
48
|
+
per_condition_timeout: Optional[float] = None
|
|
49
|
+
per_path_timeout: Optional[float] = None
|
|
50
|
+
max_iterations: Optional[int] = None
|
|
51
|
+
report_all: Optional[bool] = None
|
|
52
|
+
report_verbose: Optional[bool] = None
|
|
53
|
+
timeout: Optional[float] = None
|
|
54
|
+
max_uninteresting_iterations: Optional[int] = None
|
|
55
|
+
|
|
56
|
+
# TODO: move stats out of options
|
|
57
|
+
stats: Optional[collections.Counter] = None
|
|
58
|
+
|
|
59
|
+
# These options are the ones allowed in directives
|
|
60
|
+
directive_fields = frozenset(
|
|
61
|
+
{
|
|
62
|
+
"enabled",
|
|
63
|
+
"analysis_kind",
|
|
64
|
+
"specs_complete",
|
|
65
|
+
"max_iterations",
|
|
66
|
+
"per_condition_timeout",
|
|
67
|
+
"per_path_timeout",
|
|
68
|
+
"max_uninteresting_iterations",
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def overlay(self, overrides: "AnalysisOptionSet") -> "AnalysisOptionSet":
|
|
73
|
+
kw = {k: v for (k, v) in overrides.__dict__.items() if v is not None}
|
|
74
|
+
return replace(self, **kw)
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def parser_for(cls, field: str) -> Optional[Callable[[str], Any]]:
|
|
78
|
+
if field == "analysis_kind":
|
|
79
|
+
return _parse_analysis_kind
|
|
80
|
+
hints = get_type_hints(AnalysisOptions)
|
|
81
|
+
if field not in hints:
|
|
82
|
+
return None
|
|
83
|
+
ctor = hints[field]
|
|
84
|
+
if ctor is bool:
|
|
85
|
+
return _parse_bool
|
|
86
|
+
return ctor
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def parse_field(cls, field: str, strval: str) -> Any:
|
|
90
|
+
parser = cls.parser_for(field)
|
|
91
|
+
if parser is None:
|
|
92
|
+
return None
|
|
93
|
+
try:
|
|
94
|
+
return parser(strval)
|
|
95
|
+
except ValueError:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def option_set_from_dict(source: Mapping[str, object]) -> AnalysisOptionSet:
|
|
100
|
+
options = AnalysisOptionSet()
|
|
101
|
+
for optname in (
|
|
102
|
+
"analysis_kind",
|
|
103
|
+
"specs_complete",
|
|
104
|
+
"per_path_timeout",
|
|
105
|
+
"per_condition_timeout",
|
|
106
|
+
"max_uninteresting_iterations",
|
|
107
|
+
"report_all",
|
|
108
|
+
"report_verbose",
|
|
109
|
+
):
|
|
110
|
+
arg_val = source.get(optname, None)
|
|
111
|
+
if arg_val is not None:
|
|
112
|
+
setattr(options, optname, arg_val)
|
|
113
|
+
return options
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class AnalysisOptions:
|
|
118
|
+
"""Encodes the options for use while running CrossHair."""
|
|
119
|
+
|
|
120
|
+
analysis_kind: Sequence[AnalysisKind]
|
|
121
|
+
enabled: bool
|
|
122
|
+
specs_complete: bool
|
|
123
|
+
per_condition_timeout: float
|
|
124
|
+
max_iterations: int
|
|
125
|
+
report_all: bool
|
|
126
|
+
report_verbose: bool
|
|
127
|
+
timeout: float
|
|
128
|
+
per_path_timeout: float
|
|
129
|
+
max_uninteresting_iterations: int
|
|
130
|
+
|
|
131
|
+
# Transient members (not user-configurable):
|
|
132
|
+
deadline: float = float("NaN")
|
|
133
|
+
stats: Optional[collections.Counter] = None
|
|
134
|
+
|
|
135
|
+
def get_max_uninteresting_iterations(self):
|
|
136
|
+
max_uninteresting_iterations = self.max_uninteresting_iterations
|
|
137
|
+
if max_uninteresting_iterations == sys.maxsize and (
|
|
138
|
+
not math.isfinite(self.per_condition_timeout)
|
|
139
|
+
):
|
|
140
|
+
return 5
|
|
141
|
+
elif max_uninteresting_iterations == 0:
|
|
142
|
+
return sys.maxsize
|
|
143
|
+
else:
|
|
144
|
+
return max_uninteresting_iterations
|
|
145
|
+
|
|
146
|
+
def get_per_path_timeout(self):
|
|
147
|
+
if math.isnan(self.per_path_timeout):
|
|
148
|
+
if math.isfinite(self.per_condition_timeout):
|
|
149
|
+
if self.per_condition_timeout > 1.0:
|
|
150
|
+
return self.per_condition_timeout**0.5
|
|
151
|
+
else:
|
|
152
|
+
return self.per_condition_timeout
|
|
153
|
+
max_uninteresting_iterations = self.get_max_uninteresting_iterations()
|
|
154
|
+
if max_uninteresting_iterations < sys.maxsize:
|
|
155
|
+
return max(max_uninteresting_iterations, 1)
|
|
156
|
+
return float("inf")
|
|
157
|
+
else:
|
|
158
|
+
return self.per_path_timeout
|
|
159
|
+
|
|
160
|
+
def overlay(
|
|
161
|
+
self, overrides: Optional[AnalysisOptionSet] = None, **kw
|
|
162
|
+
) -> "AnalysisOptions":
|
|
163
|
+
if overrides is not None:
|
|
164
|
+
assert not kw
|
|
165
|
+
kw = overrides.__dict__
|
|
166
|
+
kw = {k: v for (k, v) in kw.items() if v is not None}
|
|
167
|
+
ret = replace(self, **kw)
|
|
168
|
+
assert type(ret) is AnalysisOptions
|
|
169
|
+
return ret
|
|
170
|
+
|
|
171
|
+
def split_limits(
|
|
172
|
+
self, priority: float
|
|
173
|
+
) -> Tuple["AnalysisOptions", "AnalysisOptions"]:
|
|
174
|
+
"""
|
|
175
|
+
Divide resource allotments into two.
|
|
176
|
+
|
|
177
|
+
Namely, the resource allotments (timeouts, iteration caps) are split
|
|
178
|
+
into allotments for two stages of analysis.
|
|
179
|
+
|
|
180
|
+
pre: 0.0 <= priority <= 1.0
|
|
181
|
+
post: _[0].max_iterations + _[1].max_iterations == self.max_iterations
|
|
182
|
+
"""
|
|
183
|
+
options1 = replace(
|
|
184
|
+
self,
|
|
185
|
+
per_condition_timeout=self.per_condition_timeout * priority,
|
|
186
|
+
per_path_timeout=self.per_path_timeout * priority,
|
|
187
|
+
max_iterations=round(self.max_iterations * priority),
|
|
188
|
+
)
|
|
189
|
+
inv_priority = 1.0 - priority
|
|
190
|
+
options2 = replace(
|
|
191
|
+
self,
|
|
192
|
+
per_condition_timeout=self.per_condition_timeout * inv_priority,
|
|
193
|
+
per_path_timeout=self.per_path_timeout * inv_priority,
|
|
194
|
+
max_iterations=self.max_iterations - options1.max_iterations,
|
|
195
|
+
)
|
|
196
|
+
return (options1, options2)
|
|
197
|
+
|
|
198
|
+
def incr(self, key: str):
|
|
199
|
+
if self.stats is not None:
|
|
200
|
+
self.stats[key] += 1
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
DEFAULT_OPTIONS = AnalysisOptions(
|
|
204
|
+
analysis_kind=(
|
|
205
|
+
AnalysisKind.PEP316,
|
|
206
|
+
AnalysisKind.icontract,
|
|
207
|
+
AnalysisKind.deal,
|
|
208
|
+
),
|
|
209
|
+
enabled=True,
|
|
210
|
+
specs_complete=False,
|
|
211
|
+
per_condition_timeout=float("inf"),
|
|
212
|
+
max_iterations=sys.maxsize,
|
|
213
|
+
report_all=False,
|
|
214
|
+
report_verbose=True,
|
|
215
|
+
timeout=float("inf"),
|
|
216
|
+
per_path_timeout=float("NaN"),
|
|
217
|
+
max_uninteresting_iterations=sys.maxsize,
|
|
218
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from crosshair.options import DEFAULT_OPTIONS
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_AnalysisOptions_split_limits() -> None:
|
|
5
|
+
options = DEFAULT_OPTIONS.overlay(per_path_timeout=10.0, max_iterations=16)
|
|
6
|
+
part1, part2 = options.split_limits(0.1)
|
|
7
|
+
assert part1.per_path_timeout == 1.0
|
|
8
|
+
assert part2.per_path_timeout == 9.0
|
|
9
|
+
assert part1.max_iterations == 2
|
|
10
|
+
assert part2.max_iterations == 14
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import itertools
|
|
3
|
+
import operator
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Callable, List, Mapping, Optional, Sequence, Tuple
|
|
8
|
+
|
|
9
|
+
import pytest # type: ignore
|
|
10
|
+
|
|
11
|
+
from crosshair.core import _PATCH_REGISTRATIONS
|
|
12
|
+
from crosshair.core_and_libs import standalone_statespace
|
|
13
|
+
from crosshair.test_util import ExecutionResult, summarize_execution
|
|
14
|
+
from crosshair.util import ch_stack, debug
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
Tests that the builtin and standard library patches behave like their
|
|
18
|
+
counterparts, for native python input values.
|
|
19
|
+
Equivalence under symbolic inputs is tested in "_ch_test.py" files.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
possible_args = [
|
|
24
|
+
(),
|
|
25
|
+
(0,),
|
|
26
|
+
(None,),
|
|
27
|
+
("a",),
|
|
28
|
+
("ab", "b"), # str endwith, index
|
|
29
|
+
(b"a",),
|
|
30
|
+
(b"ab", "little"), # int.from_bytes
|
|
31
|
+
("ǔ", "latin-1", "replace"), # encode, bytes constructor
|
|
32
|
+
(b"ab", b"b"), # bytes endwith, index
|
|
33
|
+
(b"x", [b"a", b"b"]), # bytes join
|
|
34
|
+
(bytearray(b"x"), [b"a", bytearray(b"b")]), # mixed bytearray join
|
|
35
|
+
([2, 1],), # min, max
|
|
36
|
+
(1, 2),
|
|
37
|
+
(int, object), # issubclass
|
|
38
|
+
(int, (str, (tuple, list))), # wacky multiply-nested issubclass checking
|
|
39
|
+
(int, 42), # issubclass error
|
|
40
|
+
(42, int), # isinstance
|
|
41
|
+
(re.compile("(ab|a|b)"), r"\n", ""), # re methods
|
|
42
|
+
(bool, [1, 1, 0]), # itertools.takewhile and friends
|
|
43
|
+
(operator.add, [1, 0], [1, 1]), # multi-iterable map
|
|
44
|
+
([(1, 2), (3, 4)]), # key-value pairs
|
|
45
|
+
([(1, 2), ([], 4)]), # key-value pairs w/ unhashable key
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
untested_patches = {
|
|
49
|
+
itertools.groupby, # the return value has nested iterators that break comparisons
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
comparisons: List[Tuple[Callable, Callable]] = []
|
|
53
|
+
for native_fn, patched_fn in _PATCH_REGISTRATIONS.items():
|
|
54
|
+
if native_fn in untested_patches:
|
|
55
|
+
continue
|
|
56
|
+
patch_name = native_fn.__name__
|
|
57
|
+
comparisons.append(
|
|
58
|
+
pytest.param(native_fn, patched_fn, id=patch_name) # type: ignore
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.mark.parametrize("native_fn,patched_fn", comparisons)
|
|
63
|
+
@pytest.mark.parametrize(
|
|
64
|
+
"args", possible_args, ids=lambda t: re.sub(r"[\W_]", "_", str(t))
|
|
65
|
+
)
|
|
66
|
+
def test_patch(native_fn: Callable, patched_fn: Callable, args: Sequence[object]):
|
|
67
|
+
debug("Patch test:", native_fn, patched_fn)
|
|
68
|
+
debug("Args:", args)
|
|
69
|
+
args2 = copy.deepcopy(args)
|
|
70
|
+
native_result = summarize_execution(native_fn, args, {}, detach_path=False)
|
|
71
|
+
debug("Native result: ", native_result)
|
|
72
|
+
with standalone_statespace:
|
|
73
|
+
patched_result = summarize_execution(patched_fn, args2, {}, detach_path=False)
|
|
74
|
+
debug("Patched result: ", patched_result)
|
|
75
|
+
assert native_result == patched_result
|