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,497 @@
1
+ import inspect
2
+ import json
3
+ import sys
4
+ from typing import List
5
+
6
+ import pytest
7
+
8
+ from crosshair.condition_parser import (
9
+ AssertsParser,
10
+ CompositeConditionParser,
11
+ DealParser,
12
+ IcontractParser,
13
+ Pep316Parser,
14
+ parse_sections,
15
+ parse_sphinx_raises,
16
+ )
17
+ from crosshair.fnutil import FunctionInfo
18
+ from crosshair.tracers import COMPOSITE_TRACER, NoTracing
19
+ from crosshair.util import AttributeHolder, debug
20
+
21
+ try:
22
+ import icontract # type: ignore
23
+ except ImportError:
24
+ icontract = None # type: ignore
25
+
26
+ try:
27
+ import deal # type: ignore
28
+ except ImportError:
29
+ deal = None # type: ignore
30
+
31
+
32
+ class LocallyDefiendException(Exception):
33
+ pass
34
+
35
+
36
+ class Foo:
37
+ """A thingy.
38
+
39
+ Examples::
40
+ >>> 'blah'
41
+ 'blah'
42
+
43
+ inv:: self.x >= 0
44
+
45
+ inv:
46
+ # a blank line with no indent is ok:
47
+
48
+ self.y >= 0
49
+ notasection:
50
+ self.z >= 0
51
+ """
52
+
53
+ x: int
54
+
55
+ def isready(self) -> bool:
56
+ """
57
+ Checks for readiness
58
+
59
+ post[]::
60
+ __return__ == (self.x == 0)
61
+ """
62
+ return self.x == 0
63
+
64
+
65
+ def single_line_condition(x: int) -> int:
66
+ """post: __return__ >= x"""
67
+ return x
68
+
69
+
70
+ def implies_condition(record: dict) -> object:
71
+ """post: implies('override' in record, _ == record['override'])"""
72
+ return record["override"] if "override" in record else 42
73
+
74
+
75
+ def locally_defined_raises_condition(record: dict) -> object:
76
+ """
77
+ raises: LocallyDefiendException
78
+ """
79
+ raise KeyError("")
80
+
81
+
82
+ def tricky_raises_condition(record: dict) -> object:
83
+ """
84
+ raises: KeyError, json.JSONDecodeError # comma , then junk
85
+ """
86
+ raise KeyError("")
87
+
88
+
89
+ def sphinx_raises(record: dict) -> object:
90
+ """
91
+ Do things.
92
+ :raises LocallyDefiendException: when blah
93
+ """
94
+ raise LocallyDefiendException("")
95
+
96
+
97
+ class BaseClassExample:
98
+ """
99
+ inv: True
100
+ """
101
+
102
+ def foo(self) -> int:
103
+ return 4
104
+
105
+
106
+ class SubClassExample(BaseClassExample):
107
+ def foo(self) -> int:
108
+ """
109
+ post: False
110
+ """
111
+ return 5
112
+
113
+
114
+ def test_parse_sections_variants() -> None:
115
+ parsed = parse_sections([(1, " :post: True ")], ("post",), "")
116
+ assert set(parsed.sections.keys()) == {"post"}
117
+ parsed = parse_sections([(1, "post::True")], ("post",), "")
118
+ assert set(parsed.sections.keys()) == {"post"}
119
+ parsed = parse_sections([(1, ":post True")], ("post",), "")
120
+ assert set(parsed.sections.keys()) == set()
121
+
122
+
123
+ def test_parse_sections_empty_vs_missing_mutations() -> None:
124
+ mutations = parse_sections([(1, "post: True")], ("post",), "").mutable_expr
125
+ assert mutations is None
126
+ mutations = parse_sections([(1, "post[]: True")], ("post",), "").mutable_expr
127
+ assert mutations == ""
128
+
129
+
130
+ def test_parse_sphinx_raises() -> None:
131
+ assert parse_sphinx_raises(sphinx_raises) == {LocallyDefiendException}
132
+
133
+
134
+ class TestPep316Parser:
135
+ def test_class_parse(self) -> None:
136
+ class_conditions = Pep316Parser().get_class_conditions(Foo)
137
+ assert set([c.expr_source for c in class_conditions.inv]) == {
138
+ "self.x >= 0",
139
+ "self.y >= 0",
140
+ }
141
+ assert {"isready", "__init__"} <= set(class_conditions.methods.keys())
142
+ method = class_conditions.methods["isready"]
143
+ assert set([c.expr_source for c in method.pre]) == {
144
+ "self.x >= 0",
145
+ "self.y >= 0",
146
+ }
147
+ startlineno = inspect.getsourcelines(Foo)[1]
148
+ assert set([(c.expr_source, c.line) for c in method.post]) == {
149
+ ("self.x >= 0", startlineno + 7),
150
+ ("self.y >= 0", startlineno + 12),
151
+ ("__return__ == (self.x == 0)", startlineno + 24),
152
+ }
153
+
154
+ def test_single_line_condition(self) -> None:
155
+ conditions = Pep316Parser().get_fn_conditions(
156
+ FunctionInfo.from_fn(single_line_condition)
157
+ )
158
+ assert conditions is not None
159
+ assert set([c.expr_source for c in conditions.post]) == {"__return__ >= x"}
160
+
161
+ def test_implies_condition(self):
162
+ conditions = Pep316Parser().get_fn_conditions(
163
+ FunctionInfo.from_fn(implies_condition)
164
+ )
165
+ assert conditions is not None
166
+ # This shouldn't explode (avoid a KeyError on record['override']):
167
+ conditions.post[0].evaluate({"record": {}, "_": 0})
168
+
169
+ def test_locally_defined_raises_condition(self) -> None:
170
+ conditions = Pep316Parser().get_fn_conditions(
171
+ FunctionInfo.from_fn(locally_defined_raises_condition)
172
+ )
173
+ assert conditions is not None
174
+ assert [] == list(conditions.syntax_messages())
175
+ assert set([LocallyDefiendException]) == conditions.raises
176
+
177
+ def test_tricky_raises_condition(self) -> None:
178
+ conditions = Pep316Parser().get_fn_conditions(
179
+ FunctionInfo.from_fn(tricky_raises_condition)
180
+ )
181
+ assert conditions is not None
182
+ assert [] == list(conditions.syntax_messages())
183
+ assert conditions.raises == {KeyError, json.JSONDecodeError}
184
+
185
+ def test_invariant_is_inherited(self) -> None:
186
+ class_conditions = Pep316Parser().get_class_conditions(SubClassExample)
187
+ assert set(class_conditions.methods.keys()) == {"foo", "__init__"}
188
+ method = class_conditions.methods["foo"]
189
+ assert len(method.pre) == 1
190
+ assert set([c.expr_source for c in method.pre]) == {"True"}
191
+ assert len(method.post) == 2
192
+ assert set([c.expr_source for c in method.post]) == {"True", "False"}
193
+
194
+ def test_invariant_applies_to_init(self) -> None:
195
+ class_conditions = Pep316Parser().get_class_conditions(BaseClassExample)
196
+ assert set(class_conditions.methods.keys()) == {"__init__", "foo"}
197
+
198
+ @pytest.mark.skipif(
199
+ sys.version_info >= (3, 13), reason="builtins have signatures in 3.13"
200
+ )
201
+ def test_builtin_conditions_are_null(self) -> None:
202
+ assert Pep316Parser().get_fn_conditions(FunctionInfo.from_fn(zip)) is None
203
+
204
+ def test_conditions_with_closure_references_and_string_type(self) -> None:
205
+ # This is a function that refers to something in its closure.
206
+ # Ensure we can still look up string-based types:
207
+ def referenced_fn():
208
+ return 4
209
+
210
+ def fn_with_closure(foo: "Foo"):
211
+ referenced_fn()
212
+
213
+ # Ensure we don't error trying to resolve "Foo":
214
+ Pep316Parser().get_fn_conditions(FunctionInfo.from_fn(fn_with_closure))
215
+
216
+
217
+ @pytest.mark.skipif(not icontract, reason="icontract is not installed")
218
+ class TestIcontractParser:
219
+ def test_simple_parse(self):
220
+ @icontract.require(lambda ls: len(ls) > 0)
221
+ @icontract.ensure(lambda ls, result: min(ls) <= result <= max(ls))
222
+ def avg(ls):
223
+ return sum(ls) / len(ls)
224
+
225
+ conditions = IcontractParser().get_fn_conditions(FunctionInfo.from_fn(avg))
226
+ assert conditions is not None
227
+ assert len(conditions.pre) == 1
228
+ assert len(conditions.post) == 1
229
+ assert conditions.pre[0].evaluate({"ls": []}) is False
230
+ post_args = {
231
+ "ls": [42, 43],
232
+ "__old__": AttributeHolder({}),
233
+ "__return__": 40,
234
+ "_": 40,
235
+ }
236
+ assert conditions.post[0].evaluate(post_args) is False
237
+ assert len(post_args) == 4 # (check args are unmodified)
238
+
239
+ def test_simple_class_parse(self):
240
+ @icontract.invariant(lambda self: self.i >= 0)
241
+ class Counter(icontract.DBC):
242
+ def __init__(self):
243
+ self.i = 0
244
+
245
+ @icontract.ensure(lambda self, result: result >= 0)
246
+ def count(self) -> int:
247
+ return self.i
248
+
249
+ @icontract.ensure(lambda self: self.count() > 0)
250
+ def incr(self):
251
+ self.i += 1
252
+
253
+ @icontract.require(lambda self: self.count() > 0)
254
+ def decr(self):
255
+ self.i -= 1
256
+
257
+ conditions = IcontractParser().get_class_conditions(Counter)
258
+ assert len(conditions.inv) == 1
259
+
260
+ decr_conditions = conditions.methods["decr"]
261
+ assert len(decr_conditions.pre) == 2
262
+ # decr() precondition: count > 0
263
+ assert decr_conditions.pre[0].evaluate({"self": Counter()}) is False
264
+ # invariant: count >= 0
265
+ assert decr_conditions.pre[1].evaluate({"self": Counter()}) is True
266
+
267
+ class TruncatedCounter(Counter):
268
+ @icontract.require(
269
+ lambda self: self.count() == 0
270
+ ) # super already allows count > 0
271
+ def decr(self):
272
+ if self.i > 0:
273
+ self.i -= 1
274
+
275
+ conditions = IcontractParser().get_class_conditions(TruncatedCounter)
276
+ decr_conditions = conditions.methods["decr"]
277
+ assert decr_conditions.pre[0].evaluate({"self": TruncatedCounter()}) is True
278
+
279
+ # check the weakened precondition
280
+ assert (
281
+ len(decr_conditions.pre) == 2
282
+ ) # one for the invariant, one for the disjunction
283
+ ctr = TruncatedCounter()
284
+ ctr.i = 1
285
+ assert decr_conditions.pre[1].evaluate({"self": ctr}) is True
286
+ assert decr_conditions.pre[0].evaluate({"self": ctr}) is True
287
+ ctr.i = 0
288
+ assert decr_conditions.pre[1].evaluate({"self": ctr}) is True
289
+ assert decr_conditions.pre[0].evaluate({"self": ctr}) is True
290
+
291
+
292
+ @pytest.mark.skipif(not deal, reason="deal is not installed")
293
+ def test_deal_basics():
294
+ @deal.raises(ZeroDivisionError)
295
+ @deal.pre(lambda a, b: a >= 0 and b >= 0)
296
+ @deal.ensure(lambda a, b, result: result <= a)
297
+ def f(a: int, b: int) -> float:
298
+ return a / b
299
+
300
+ conditions = DealParser().get_fn_conditions(FunctionInfo.from_fn(f))
301
+ (pre,) = conditions.pre
302
+ (post,) = conditions.post
303
+
304
+ assert conditions.fn(12, b=6) == 2.0
305
+ assert conditions.raises == {ZeroDivisionError}
306
+ assert pre.evaluate({"a": -2, "b": 3}) == False # noqa: E712
307
+ assert pre.evaluate({"a": 2, "b": 3}) == True # noqa: E712
308
+ post_args = {
309
+ "a": 6,
310
+ "b": 2,
311
+ "__old__": AttributeHolder({}),
312
+ "_": 3.0,
313
+ "__return__": 3.0,
314
+ }
315
+ assert post.evaluate(post_args) == True # noqa: E712
316
+
317
+
318
+ @pytest.mark.skipif(not deal, reason="deal is not installed")
319
+ def test_deal_postcondition():
320
+ @deal.raises(ZeroDivisionError)
321
+ @deal.post(lambda r: r >= 0)
322
+ def f(a: int, b: int) -> float:
323
+ return a / b
324
+
325
+ conditions = DealParser().get_fn_conditions(FunctionInfo.from_fn(f))
326
+ (post,) = conditions.post
327
+
328
+ post_args = {
329
+ "a": 6,
330
+ "b": 2,
331
+ "__old__": AttributeHolder({}),
332
+ "_": 3.0,
333
+ "__return__": 3.0,
334
+ }
335
+ assert post.evaluate(post_args) == True # noqa: E712
336
+ post_args["__return__"] = -1.0
337
+ assert post.evaluate(post_args) == False # noqa: E712
338
+
339
+
340
+ @pytest.mark.skipif(not deal, reason="deal is not installed")
341
+ def test_deal_ensure_with_magic_single_arg():
342
+ @deal.ensure(lambda _: _.result == 0 or _["item"] in _["items"])
343
+ @deal.pure
344
+ def count(items: List[str], item: str) -> int:
345
+ return items.count(item)
346
+
347
+ conditions = DealParser().get_fn_conditions(FunctionInfo.from_fn(count))
348
+ (post,) = conditions.post
349
+ post_args = {
350
+ "item": "a",
351
+ "items": ["b", "c"],
352
+ "__old__": AttributeHolder({}),
353
+ "_": 1,
354
+ "__return__": 1,
355
+ }
356
+ assert post.evaluate(post_args) == False # noqa: E712
357
+
358
+
359
+ def avg_with_asserts(items: List[float]) -> float:
360
+ assert items
361
+ avgval = sum(items) / len(items)
362
+ assert avgval <= 10
363
+ return avgval
364
+
365
+
366
+ def no_leading_assert(x: int) -> int:
367
+ x = x + 1
368
+ assert x != 100
369
+ x = x + 1
370
+ return x
371
+
372
+
373
+ def fn_with_docstring_comments_and_assert(numbers: List[int]) -> None:
374
+ """Removes the smallest number in the given list."""
375
+ # The precondition: CrossHair will assume this to be true:
376
+ assert len(numbers) > 0
377
+ smallest = min(numbers)
378
+ numbers.remove(smallest)
379
+ # The postcondition: CrossHair will find examples to make this be false:
380
+ assert min(numbers) > smallest
381
+
382
+
383
+ class TestAssertsParser:
384
+ def tests_simple_parse(self) -> None:
385
+ conditions = AssertsParser().get_fn_conditions(
386
+ FunctionInfo.from_fn(avg_with_asserts)
387
+ )
388
+ assert conditions is not None
389
+ conditions.fn([])
390
+ assert conditions.fn([2.2]) == 2.2
391
+ with pytest.raises(AssertionError):
392
+ conditions.fn([9.2, 17.8])
393
+
394
+ def tests_empty_parse(self) -> None:
395
+ conditions = AssertsParser().get_fn_conditions(FunctionInfo.from_fn(debug))
396
+ assert conditions is None
397
+
398
+ def tests_extra_ast_nodes(self) -> None:
399
+ conditions = AssertsParser().get_fn_conditions(
400
+ FunctionInfo.from_fn(fn_with_docstring_comments_and_assert)
401
+ )
402
+ assert conditions is not None
403
+
404
+ # Empty list does not pass precondition, ignored:
405
+ conditions.fn([])
406
+
407
+ # normal, passing case:
408
+ nums = [3, 1, 2]
409
+ conditions.fn(nums)
410
+ assert nums == [3, 2]
411
+
412
+ # Failing case (duplicate minimum values):
413
+ with pytest.raises(AssertionError):
414
+ nums = [3, 1, 1, 2]
415
+ conditions.fn(nums)
416
+
417
+
418
+ def test_CompositeConditionParser():
419
+ composite = CompositeConditionParser()
420
+ composite.parsers.append(Pep316Parser(composite))
421
+ composite.parsers.append(AssertsParser(composite))
422
+ assert composite.get_fn_conditions(
423
+ FunctionInfo.from_fn(single_line_condition)
424
+ ).has_any()
425
+ assert composite.get_fn_conditions(FunctionInfo.from_fn(avg_with_asserts)).has_any()
426
+
427
+
428
+ def no_postconditions(items: List[float]) -> float:
429
+ """pre: items"""
430
+ return sum(items) / len(items)
431
+
432
+
433
+ def test_adds_completion_postconditions():
434
+ pep316_parser = Pep316Parser()
435
+ fn = FunctionInfo.from_fn(no_postconditions)
436
+ assert len(pep316_parser.get_fn_conditions(fn).post) == 1
437
+
438
+
439
+ def test_raw_docstring():
440
+ def linelen(s: str) -> int:
441
+ r"""
442
+ pre: '\n' not in s
443
+ """
444
+ return len(s)
445
+
446
+ conditions = Pep316Parser().get_fn_conditions(FunctionInfo.from_fn(linelen))
447
+ assert len(conditions.pre) == 1
448
+ assert conditions.pre[0].expr_source == r"'\n' not in s"
449
+
450
+
451
+ def test_regular_docstrings_parsed_like_raw():
452
+ def linelen(s: str) -> int:
453
+ """pre: '\n' not in s"""
454
+ return len(s)
455
+
456
+ conditions = Pep316Parser().get_fn_conditions(FunctionInfo.from_fn(linelen))
457
+ assert len(conditions.pre) == 1
458
+ assert conditions.pre[0].expr_source == r"'\n' not in s"
459
+
460
+
461
+ def test_lines_with_trailing_comment():
462
+ def foo():
463
+ """
464
+ post: True""" # A trailing comment
465
+ ...
466
+
467
+ conditions = Pep316Parser().get_fn_conditions(FunctionInfo.from_fn(foo))
468
+ assert len(conditions.post) == 1
469
+ assert conditions.post[0].expr_source == "True"
470
+
471
+
472
+ def test_format_counterexample_positional_only():
473
+ if sys.version_info >= (3, 8):
474
+
475
+ def foo(a=10, /, b=20):
476
+ """post: True"""
477
+
478
+ args = inspect.BoundArguments(inspect.signature(foo), {"a": 1, "b": 2})
479
+ conditions = Pep316Parser().get_fn_conditions(FunctionInfo.from_fn(foo))
480
+ assert conditions.format_counterexample(args, None, {}) == (
481
+ "foo(1, b=2)",
482
+ "None",
483
+ )
484
+
485
+
486
+ def test_format_counterexample_keyword_only():
487
+ def foo(a, *, b):
488
+ """post: True"""
489
+
490
+ args = inspect.BoundArguments(inspect.signature(foo), {"a": 1, "b": 2})
491
+ conditions = Pep316Parser().get_fn_conditions(FunctionInfo.from_fn(foo))
492
+ assert conditions
493
+ with COMPOSITE_TRACER, NoTracing():
494
+ assert conditions.format_counterexample(args, None, {}) == (
495
+ "foo(1, b=2)",
496
+ "None",
497
+ )
crosshair/conftest.py ADDED
@@ -0,0 +1,30 @@
1
+ from sys import argv
2
+
3
+ import pytest
4
+
5
+ from crosshair.core_and_libs import NoTracing, standalone_statespace
6
+ from crosshair.util import mem_usage_kb, set_debug
7
+
8
+
9
+ def pytest_configure(config):
10
+ if "-v" in argv or "-vv" in argv:
11
+ set_debug(True)
12
+
13
+
14
+ LEAK_LIMIT_KB = 400 * 1024
15
+
16
+
17
+ @pytest.hookimpl(hookwrapper=True)
18
+ def pytest_pyfunc_call(pyfuncitem):
19
+ last_ram = mem_usage_kb()
20
+ outcome = yield
21
+ growth = mem_usage_kb() - last_ram
22
+ assert (
23
+ growth < LEAK_LIMIT_KB
24
+ ), f"Leaking memory (grew {growth // 1024}M while running)"
25
+
26
+
27
+ @pytest.fixture()
28
+ def space():
29
+ with standalone_statespace as spc, NoTracing():
30
+ yield spc
crosshair/copyext.py ADDED
@@ -0,0 +1,155 @@
1
+ import sys
2
+
3
+ if sys.version_info >= (3, 14):
4
+ from copy import _atomic_types
5
+ else:
6
+ from copy import _deepcopy_atomic # type: ignore
7
+ from copy import _deepcopy_dict # type: ignore
8
+ from copy import _deepcopy_dispatch # type: ignore
9
+ from copy import _deepcopy_list # type: ignore
10
+ from copy import _deepcopy_tuple # type: ignore
11
+ from copy import _keep_alive # type: ignore
12
+ from copy import _reconstruct # type: ignore
13
+ from copy import Error
14
+ from copyreg import dispatch_table # type: ignore
15
+ from enum import Enum
16
+ from types import MappingProxyType
17
+ from typing import Any, Callable, Dict, Tuple
18
+
19
+ from crosshair.tracers import ResumedTracing
20
+ from crosshair.util import (
21
+ CrossHairInternal,
22
+ IdKeyedDict,
23
+ assert_tracing,
24
+ ch_stack,
25
+ debug,
26
+ )
27
+
28
+ _MISSING = object
29
+
30
+
31
+ class CopyMode(int, Enum):
32
+ REGULAR = 0
33
+ BEST_EFFORT = 1
34
+ REALIZE = 2
35
+
36
+
37
+ # We need to be able to realize some types that are not deep-copyable.
38
+ # Such realization overrides are defined here.
39
+ # TODO: This capability should probably be something that plugins can extend
40
+ _DEEP_REALIZATION_OVERRIDES = IdKeyedDict()
41
+ _DEEP_REALIZATION_OVERRIDES[MappingProxyType] = lambda p, m: MappingProxyType(
42
+ deepcopyext(dict(p), CopyMode.REALIZE, m)
43
+ )
44
+ if sys.version_info >= (3, 10):
45
+ _DEEP_REALIZATION_OVERRIDES[type({}.items())] = lambda p, m: deepcopyext(
46
+ p.mapping, CopyMode.REALIZE, m
47
+ ).items()
48
+ _DEEP_REALIZATION_OVERRIDES[type({}.keys())] = lambda p, m: deepcopyext(
49
+ p.mapping, CopyMode.REALIZE, m
50
+ ).keys()
51
+ _DEEP_REALIZATION_OVERRIDES[type({}.values())] = lambda p, m: deepcopyext(
52
+ p.mapping, CopyMode.REALIZE, m
53
+ ).values()
54
+
55
+
56
+ @assert_tracing(False)
57
+ def deepcopyext(obj: object, mode: CopyMode, memo: Dict) -> Any:
58
+ objid = id(obj)
59
+ cpy = memo.get(objid, _MISSING)
60
+ if cpy is not _MISSING:
61
+ if objid not in map(id, memo.get(id(memo), ())):
62
+ # we are trying to return some value that was not kept alive;
63
+ # it may have been garbage collected and replaced.
64
+ raise CrossHairInternal("Possibly transient value found in memo")
65
+ else:
66
+ _keep_alive(obj, memo)
67
+ deepconstruct_obj = obj
68
+ if mode == CopyMode.REALIZE:
69
+ cls = type(obj)
70
+ if hasattr(cls, "__ch_deep_realize__"):
71
+ cpy = obj.__ch_deep_realize__(memo) # type: ignore
72
+ elif hasattr(cls, "__ch_realize__"):
73
+ # Do shallow realization here, and then fall through to
74
+ # _deepconstruct below.
75
+ deepconstruct_obj = obj.__ch_realize__() # type: ignore
76
+ # this transient object may be inserted into memo below
77
+ _keep_alive(deepconstruct_obj, memo)
78
+ else:
79
+ realization_override = _DEEP_REALIZATION_OVERRIDES.get(cls)
80
+ if realization_override:
81
+ cpy = realization_override(obj, memo)
82
+ if cpy is _MISSING:
83
+ try:
84
+ cpy = _deepconstruct(deepconstruct_obj, mode, memo)
85
+ except TypeError as exc:
86
+ if mode == CopyMode.REGULAR:
87
+ raise
88
+ debug(
89
+ "Cannot copy object of type",
90
+ type(obj),
91
+ "ignoring",
92
+ type(exc),
93
+ ":",
94
+ str(exc),
95
+ "at",
96
+ ch_stack(currently_handling=exc),
97
+ )
98
+ cpy = deepconstruct_obj
99
+ memo[objid] = cpy
100
+ return cpy
101
+
102
+
103
+ if sys.version_info >= (3, 14):
104
+
105
+ def lookup_dispatch(cls: type) -> Callable:
106
+ if cls in _atomic_types:
107
+ return lambda obj, memo: obj
108
+ return _deepcopy_dispatch.get(cls)
109
+
110
+ else:
111
+
112
+ def lookup_dispatch(cls: type) -> Callable:
113
+ return _deepcopy_dispatch.get(cls)
114
+
115
+
116
+ def _deepconstruct(obj: object, mode: CopyMode, memo: Dict):
117
+ cls = type(obj)
118
+
119
+ def subdeepcopy(obj: object, memo: Dict):
120
+ return deepcopyext(obj, mode, memo)
121
+
122
+ creator = lookup_dispatch(cls)
123
+ if creator is not None:
124
+ if creator in (_deepcopy_dict, _deepcopy_list, _deepcopy_tuple):
125
+ return creator(obj, memo, deepcopy=subdeepcopy)
126
+ else:
127
+ # TODO: We loose subdeepcopy in this case - won't
128
+ # that make e.g. deep_realize be too shallow?
129
+ return creator(obj, memo)
130
+ if isinstance(obj, type):
131
+ return obj
132
+ if mode != CopyMode.REALIZE and hasattr(obj, "__deepcopy__"):
133
+ return obj.__deepcopy__(memo) # type: ignore
134
+ if cls in dispatch_table:
135
+ to_call = dispatch_table[cls]
136
+ call_args: Tuple = (obj,)
137
+ elif hasattr(cls, "__reduce_ex__"):
138
+ to_call = getattr(cls, "__reduce_ex__")
139
+ call_args = (obj, 4)
140
+ elif hasattr(cls, "__reduce__"):
141
+ to_call = getattr(cls, "__reduce__")
142
+ call_args = (obj,)
143
+ else:
144
+ raise Error("un(deep)copyable object of type %s" % cls)
145
+ if (
146
+ getattr(cls, "__reduce__") is object.__reduce__
147
+ and getattr(cls, "__reduce_ex__") is object.__reduce_ex__
148
+ ):
149
+ reduct = to_call(*call_args)
150
+ else:
151
+ with ResumedTracing():
152
+ reduct = to_call(*call_args)
153
+ if isinstance(reduct, str):
154
+ return obj
155
+ return _reconstruct(obj, memo, *reduct, deepcopy=subdeepcopy)