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,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)
|