crosshair-tool 0.0.99__cp312-cp312-macosx_10_13_x86_64.whl

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