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
crosshair/core_test.py
ADDED
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import importlib
|
|
3
|
+
import inspect
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from typing import *
|
|
8
|
+
|
|
9
|
+
import pytest # type: ignore
|
|
10
|
+
|
|
11
|
+
import crosshair
|
|
12
|
+
from crosshair import core_and_libs, type_repo
|
|
13
|
+
from crosshair.core import (
|
|
14
|
+
deep_realize,
|
|
15
|
+
get_constructor_signature,
|
|
16
|
+
is_deeply_immutable,
|
|
17
|
+
proxy_for_class,
|
|
18
|
+
proxy_for_type,
|
|
19
|
+
run_checkables,
|
|
20
|
+
)
|
|
21
|
+
from crosshair.core_and_libs import (
|
|
22
|
+
AnalysisKind,
|
|
23
|
+
List,
|
|
24
|
+
MessageType,
|
|
25
|
+
analyze_any,
|
|
26
|
+
analyze_class,
|
|
27
|
+
analyze_function,
|
|
28
|
+
standalone_statespace,
|
|
29
|
+
)
|
|
30
|
+
from crosshair.fnutil import FunctionInfo, walk_qualname
|
|
31
|
+
from crosshair.libimpl.builtinslib import LazyIntSymbolicStr, SymbolicInt
|
|
32
|
+
from crosshair.options import DEFAULT_OPTIONS, AnalysisOptionSet
|
|
33
|
+
from crosshair.statespace import (
|
|
34
|
+
CANNOT_CONFIRM,
|
|
35
|
+
CONFIRMED,
|
|
36
|
+
EXEC_ERR,
|
|
37
|
+
POST_ERR,
|
|
38
|
+
POST_FAIL,
|
|
39
|
+
)
|
|
40
|
+
from crosshair.test_util import check_exec_err, check_messages, check_states
|
|
41
|
+
from crosshair.tracers import NoTracing, ResumedTracing, is_tracing
|
|
42
|
+
from crosshair.util import CrossHairInternal, set_debug
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
import icontract
|
|
46
|
+
except Exception:
|
|
47
|
+
icontract = None # type: ignore
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.fixture(autouse=True)
|
|
51
|
+
def check_tracer_state():
|
|
52
|
+
assert not is_tracing()
|
|
53
|
+
yield None
|
|
54
|
+
assert not is_tracing()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclasses.dataclass
|
|
58
|
+
class Pokeable:
|
|
59
|
+
"""
|
|
60
|
+
inv: self.x >= 0
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
x: int = 1
|
|
64
|
+
|
|
65
|
+
def poke(self) -> None:
|
|
66
|
+
"""
|
|
67
|
+
post[self]: True
|
|
68
|
+
"""
|
|
69
|
+
self.x += 1
|
|
70
|
+
|
|
71
|
+
def wild_pokeby(self, amount: int) -> None:
|
|
72
|
+
"""
|
|
73
|
+
post[self]: True
|
|
74
|
+
"""
|
|
75
|
+
self.x += amount
|
|
76
|
+
|
|
77
|
+
def safe_pokeby(self, amount: int) -> None:
|
|
78
|
+
"""
|
|
79
|
+
pre: amount >= 0
|
|
80
|
+
post[self]: True
|
|
81
|
+
"""
|
|
82
|
+
self.x += amount
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def remove_smallest_with_asserts(numbers: List[int]) -> None:
|
|
86
|
+
assert len(numbers) > 0
|
|
87
|
+
smallest = min(numbers)
|
|
88
|
+
numbers.remove(smallest)
|
|
89
|
+
assert len(numbers) == 0 or min(numbers) > smallest
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if icontract:
|
|
93
|
+
|
|
94
|
+
@icontract.snapshot(lambda lst: lst[:])
|
|
95
|
+
@icontract.ensure(lambda OLD, lst, value: lst == OLD.lst + [value])
|
|
96
|
+
def icontract_appender(lst: List[int], value: int) -> None:
|
|
97
|
+
lst.append(value)
|
|
98
|
+
lst.append(1984) # bug
|
|
99
|
+
|
|
100
|
+
@icontract.invariant(lambda self: self.x > 0)
|
|
101
|
+
class IcontractA(icontract.DBC):
|
|
102
|
+
def __init__(self) -> None:
|
|
103
|
+
self.x = 10
|
|
104
|
+
|
|
105
|
+
@icontract.require(lambda x: x % 2 == 0)
|
|
106
|
+
def weakenedfunc(self, x: int) -> None:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
@icontract.invariant(lambda self: self.x < 100)
|
|
110
|
+
class IcontractB(IcontractA):
|
|
111
|
+
def break_parent_invariant(self):
|
|
112
|
+
self.x = -1
|
|
113
|
+
|
|
114
|
+
def break_my_invariant(self):
|
|
115
|
+
self.x = 101
|
|
116
|
+
|
|
117
|
+
@icontract.require(lambda x: x % 3 == 0)
|
|
118
|
+
def weakenedfunc(self, x: int) -> None:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ShippingContainer:
|
|
123
|
+
container_weight = 4
|
|
124
|
+
|
|
125
|
+
def total_weight(self) -> int:
|
|
126
|
+
"""post: _ < 10"""
|
|
127
|
+
return self.cargo_weight() + self.container_weight
|
|
128
|
+
|
|
129
|
+
def cargo_weight(self) -> int:
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
def __repr__(self):
|
|
133
|
+
return type(self).__name__ + "()"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class OverloadedContainer(ShippingContainer):
|
|
137
|
+
"""
|
|
138
|
+
We use this example to demonstrate messaging when an override breaks
|
|
139
|
+
the contract of a different method.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def cargo_weight(self) -> int:
|
|
143
|
+
return 9
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class Cat:
|
|
147
|
+
def size(self) -> int:
|
|
148
|
+
return 1
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class BiggerCat(Cat):
|
|
152
|
+
def size(self) -> int:
|
|
153
|
+
return 2
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class PersonTuple(NamedTuple):
|
|
157
|
+
name: str
|
|
158
|
+
age: int
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class PersonWithoutAttributes:
|
|
162
|
+
def __init__(self, name: str, age: int):
|
|
163
|
+
self.name = name
|
|
164
|
+
self.age = age
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
NOW = 1000
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclasses.dataclass
|
|
171
|
+
class Person:
|
|
172
|
+
"""
|
|
173
|
+
Contains various features that we expect to be successfully checkable.
|
|
174
|
+
|
|
175
|
+
inv: True
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
name: str
|
|
179
|
+
birth: int
|
|
180
|
+
|
|
181
|
+
def _getage(self):
|
|
182
|
+
return NOW - self.birth
|
|
183
|
+
|
|
184
|
+
def _setage(self, newage):
|
|
185
|
+
self.birth = NOW - newage
|
|
186
|
+
|
|
187
|
+
def _delage(self):
|
|
188
|
+
del self.birth
|
|
189
|
+
|
|
190
|
+
age = property(_getage, _setage, _delage, "Age of person")
|
|
191
|
+
|
|
192
|
+
def abstract_operation(self):
|
|
193
|
+
"""
|
|
194
|
+
post: False # doesn't error because the method is "abstract"
|
|
195
|
+
"""
|
|
196
|
+
raise NotImplementedError
|
|
197
|
+
|
|
198
|
+
def a_regular_method(self):
|
|
199
|
+
"""post: True"""
|
|
200
|
+
|
|
201
|
+
@classmethod
|
|
202
|
+
def a_class_method(cls, x):
|
|
203
|
+
"""post: cls == Person"""
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def a_static_method():
|
|
207
|
+
"""post: True"""
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class AirSample:
|
|
211
|
+
# NOTE: we don't use an enum here because we want to use pure symbolic containers
|
|
212
|
+
# in our tests.
|
|
213
|
+
CLEAN = 0
|
|
214
|
+
SMOKE = 1
|
|
215
|
+
CO2 = 2
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@dataclasses.dataclass
|
|
219
|
+
class SmokeDetector:
|
|
220
|
+
"""inv: not (self._is_plugged_in and self._in_original_packaging)"""
|
|
221
|
+
|
|
222
|
+
_in_original_packaging: bool
|
|
223
|
+
_is_plugged_in: bool
|
|
224
|
+
|
|
225
|
+
def signaling_alarm(self, air_samples: List[int]) -> bool:
|
|
226
|
+
"""
|
|
227
|
+
pre: self._is_plugged_in
|
|
228
|
+
post: implies(AirSample.SMOKE in air_samples, _ == True)
|
|
229
|
+
"""
|
|
230
|
+
return AirSample.SMOKE in air_samples
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class Measurer:
|
|
234
|
+
def measure(self, x: int) -> str:
|
|
235
|
+
"""
|
|
236
|
+
post: _ == self.measure(-x)
|
|
237
|
+
"""
|
|
238
|
+
return "small" if x <= 10 else "large"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _unused_fn(x: int) -> "ClassWithExplicitSignature":
|
|
242
|
+
raise NotImplementedError
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class ClassWithExplicitSignature:
|
|
246
|
+
__signature__ = inspect.signature(_unused_fn)
|
|
247
|
+
|
|
248
|
+
def __init__(self, *a):
|
|
249
|
+
self.x = a[0]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
A_REFERENCED_THING = 42
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@dataclasses.dataclass(repr=False)
|
|
256
|
+
class ReferenceHoldingClass:
|
|
257
|
+
"""
|
|
258
|
+
inv: self.item != A_REFERENCED_THING
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
item: str
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def fibb(x: int) -> int:
|
|
265
|
+
"""
|
|
266
|
+
pre: x>=0
|
|
267
|
+
post[]: _ < 3
|
|
268
|
+
"""
|
|
269
|
+
if x <= 2:
|
|
270
|
+
return 1
|
|
271
|
+
r1, r2 = fibb(x - 1), fibb(x - 2)
|
|
272
|
+
ret = r1 + r2
|
|
273
|
+
return ret
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def reentrant_precondition(minx: int):
|
|
277
|
+
"""pre: reentrant_precondition(minx - 1)"""
|
|
278
|
+
return minx <= 10
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def recursive_example(x: int) -> bool:
|
|
282
|
+
"""
|
|
283
|
+
pre: x >= 0
|
|
284
|
+
post[]:
|
|
285
|
+
__old__.x >= 0 # just to confirm __old__ works in recursive cases
|
|
286
|
+
_ == True
|
|
287
|
+
"""
|
|
288
|
+
if x == 0:
|
|
289
|
+
return True
|
|
290
|
+
else:
|
|
291
|
+
return recursive_example(x - 1)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class RegularInt:
|
|
295
|
+
def __new__(self, num: "int"):
|
|
296
|
+
return num
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def test_get_constructor_signature_with_new():
|
|
300
|
+
assert RegularInt(7) == 7
|
|
301
|
+
params = get_constructor_signature(RegularInt).parameters
|
|
302
|
+
assert len(params) == 1
|
|
303
|
+
assert params["num"].name == "num"
|
|
304
|
+
assert params["num"].annotation == int
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_proxy_alone() -> None:
|
|
308
|
+
def f(pokeable: Pokeable) -> None:
|
|
309
|
+
"""
|
|
310
|
+
post[pokeable]: pokeable.x > 0
|
|
311
|
+
"""
|
|
312
|
+
pokeable.poke()
|
|
313
|
+
|
|
314
|
+
check_states(f, CONFIRMED)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def test_proxy_in_list() -> None:
|
|
318
|
+
def f(pokeables: List[Pokeable]) -> None:
|
|
319
|
+
"""
|
|
320
|
+
pre: len(pokeables) == 1
|
|
321
|
+
post: all(p.x > 0 for p in pokeables)
|
|
322
|
+
"""
|
|
323
|
+
for pokeable in pokeables:
|
|
324
|
+
pokeable.poke()
|
|
325
|
+
|
|
326
|
+
check_states(f, CONFIRMED)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_class_with_explicit_signature() -> None:
|
|
330
|
+
def f(c: ClassWithExplicitSignature) -> int:
|
|
331
|
+
"""post: _ != 42"""
|
|
332
|
+
return c.x
|
|
333
|
+
|
|
334
|
+
# pydantic sets __signature__ on the class, so we look for that as well as on
|
|
335
|
+
# __init__ (see https://github.com/samuelcolvin/pydantic/pull/1034)
|
|
336
|
+
check_states(f, POST_FAIL)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def test_preconditioned_init():
|
|
340
|
+
class Penguin:
|
|
341
|
+
_age: int
|
|
342
|
+
|
|
343
|
+
def __init__(self, age: int):
|
|
344
|
+
"""pre: age >= 1"""
|
|
345
|
+
self._age = age
|
|
346
|
+
|
|
347
|
+
def f(p: Penguin) -> int:
|
|
348
|
+
"""post: _ != 0"""
|
|
349
|
+
return p._age
|
|
350
|
+
|
|
351
|
+
check_states(f, CONFIRMED)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def test_class_proxies_are_created_through_constructor():
|
|
355
|
+
class Penguin:
|
|
356
|
+
can_swim: bool
|
|
357
|
+
|
|
358
|
+
def __init__(self):
|
|
359
|
+
self.can_swim = True
|
|
360
|
+
|
|
361
|
+
with standalone_statespace as space:
|
|
362
|
+
with NoTracing(): # (because following function resumes tracing)
|
|
363
|
+
p = proxy_for_class(Penguin, "p")
|
|
364
|
+
# `can_swim` is locked to True
|
|
365
|
+
assert p.can_swim is True
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def test_exc_handling_doesnt_catch_crosshair_timeout():
|
|
369
|
+
def f(x: int, y: int):
|
|
370
|
+
"""post: True"""
|
|
371
|
+
try:
|
|
372
|
+
while x + y <= x + y:
|
|
373
|
+
x += 1
|
|
374
|
+
except Exception as exc:
|
|
375
|
+
raise ValueError(f"I gobble exceptions!")
|
|
376
|
+
|
|
377
|
+
check_states(
|
|
378
|
+
f,
|
|
379
|
+
CANNOT_CONFIRM,
|
|
380
|
+
AnalysisOptionSet(per_condition_timeout=0.5, per_path_timeout=0.5),
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def test_obj_member_fail() -> None:
|
|
385
|
+
def f(foo: Pokeable) -> int:
|
|
386
|
+
"""
|
|
387
|
+
pre: 0 <= foo.x <= 4
|
|
388
|
+
post[foo]: _ < 5
|
|
389
|
+
"""
|
|
390
|
+
foo.poke()
|
|
391
|
+
foo.poke()
|
|
392
|
+
return foo.x
|
|
393
|
+
|
|
394
|
+
check_states(f, POST_FAIL)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def test_obj_member_nochange_ok() -> None:
|
|
398
|
+
def f(foo: Pokeable) -> int:
|
|
399
|
+
"""post: _ == foo.x"""
|
|
400
|
+
return foo.x
|
|
401
|
+
|
|
402
|
+
check_states(f, CONFIRMED)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def test_obj_member_change_ok() -> None:
|
|
406
|
+
def f(foo: Pokeable) -> int:
|
|
407
|
+
"""
|
|
408
|
+
pre: foo.x >= 0
|
|
409
|
+
post[foo]: foo.x >= 2
|
|
410
|
+
"""
|
|
411
|
+
foo.poke()
|
|
412
|
+
foo.poke()
|
|
413
|
+
return foo.x
|
|
414
|
+
|
|
415
|
+
check_states(f, CONFIRMED)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def test_obj_member_change_detect() -> None:
|
|
419
|
+
def f(foo: Pokeable) -> int:
|
|
420
|
+
"""
|
|
421
|
+
pre: foo.x > 0
|
|
422
|
+
post[]: True
|
|
423
|
+
"""
|
|
424
|
+
foo.poke()
|
|
425
|
+
return foo.x
|
|
426
|
+
|
|
427
|
+
check_states(f, POST_ERR)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def test_example_second_largest() -> None:
|
|
431
|
+
def second_largest(items: List[int]) -> int:
|
|
432
|
+
"""
|
|
433
|
+
pre: len(items) == 3 # (length is to cap runtime)
|
|
434
|
+
post: _ == sorted(items)[-2]
|
|
435
|
+
"""
|
|
436
|
+
next_largest, largest = items[:2]
|
|
437
|
+
if largest < next_largest:
|
|
438
|
+
next_largest, largest = largest, next_largest
|
|
439
|
+
|
|
440
|
+
for item in items[2:]:
|
|
441
|
+
if item > largest:
|
|
442
|
+
largest, next_largest = (item, largest)
|
|
443
|
+
elif item > next_largest:
|
|
444
|
+
next_largest = item
|
|
445
|
+
return next_largest
|
|
446
|
+
|
|
447
|
+
check_states(second_largest, CONFIRMED)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def test_pokeable_class() -> None:
|
|
451
|
+
messages = analyze_class(Pokeable)
|
|
452
|
+
line = Pokeable.wild_pokeby.__code__.co_firstlineno
|
|
453
|
+
actual, expected = check_messages(
|
|
454
|
+
messages, state=MessageType.POST_FAIL, line=line, column=0
|
|
455
|
+
)
|
|
456
|
+
assert actual == expected
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def test_person_class() -> None:
|
|
460
|
+
messages = analyze_class(Person)
|
|
461
|
+
actual, expected = check_messages(messages, state=MessageType.CONFIRMED)
|
|
462
|
+
assert actual == expected
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def test_methods_directly() -> None:
|
|
466
|
+
# Running analysis on individual methods directly works a little
|
|
467
|
+
# differently, especially for staticmethod/classmethod. Confirm these
|
|
468
|
+
# don't explode:
|
|
469
|
+
messages = analyze_any(
|
|
470
|
+
walk_qualname(Person, "a_regular_method"),
|
|
471
|
+
AnalysisOptionSet(),
|
|
472
|
+
)
|
|
473
|
+
actual, expected = check_messages(messages, state=MessageType.CONFIRMED)
|
|
474
|
+
assert actual == expected
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def test_class_method() -> None:
|
|
478
|
+
messages = analyze_any(
|
|
479
|
+
walk_qualname(Person, "a_class_method"),
|
|
480
|
+
AnalysisOptionSet(),
|
|
481
|
+
)
|
|
482
|
+
actual, expected = check_messages(messages, state=MessageType.CONFIRMED)
|
|
483
|
+
assert actual == expected
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def test_static_method() -> None:
|
|
487
|
+
messages = analyze_any(
|
|
488
|
+
walk_qualname(Person, "a_static_method"),
|
|
489
|
+
AnalysisOptionSet(),
|
|
490
|
+
)
|
|
491
|
+
actual, expected = check_messages(messages, state=MessageType.CONFIRMED)
|
|
492
|
+
assert actual == expected
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def test_extend_namedtuple() -> None:
|
|
496
|
+
def f(p: PersonTuple) -> PersonTuple:
|
|
497
|
+
"""
|
|
498
|
+
post: _.age != 222
|
|
499
|
+
"""
|
|
500
|
+
return PersonTuple(p.name, p.age + 1)
|
|
501
|
+
|
|
502
|
+
check_states(f, POST_FAIL)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def test_without_typed_attributes() -> None:
|
|
506
|
+
def f(p: PersonWithoutAttributes) -> PersonWithoutAttributes:
|
|
507
|
+
"""
|
|
508
|
+
post: _.age != 222
|
|
509
|
+
"""
|
|
510
|
+
return PersonTuple(p.name, p.age + 1) # type: ignore
|
|
511
|
+
|
|
512
|
+
check_states(f, POST_FAIL)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def test_property() -> None:
|
|
516
|
+
def f(p: Person) -> None:
|
|
517
|
+
"""
|
|
518
|
+
pre: 0 <= p.age < 100
|
|
519
|
+
post[p]: p.birth + p.age == NOW
|
|
520
|
+
"""
|
|
521
|
+
assert p.age == NOW - p.birth
|
|
522
|
+
oldbirth = p.birth
|
|
523
|
+
p.age = p.age + 1
|
|
524
|
+
assert oldbirth == p.birth + 1
|
|
525
|
+
|
|
526
|
+
check_states(f, CONFIRMED)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def test_readonly_property_contract() -> None:
|
|
530
|
+
class Clock:
|
|
531
|
+
@property
|
|
532
|
+
def time(self) -> int:
|
|
533
|
+
"""post: _ == self.time"""
|
|
534
|
+
return 120
|
|
535
|
+
|
|
536
|
+
messages = analyze_class(Clock)
|
|
537
|
+
actual, expected = check_messages(messages, state=MessageType.CONFIRMED)
|
|
538
|
+
assert actual == expected
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def test_typevar_basic() -> None:
|
|
542
|
+
T = TypeVar("T")
|
|
543
|
+
|
|
544
|
+
@dataclasses.dataclass
|
|
545
|
+
class MaybePair(Generic[T]):
|
|
546
|
+
"""
|
|
547
|
+
inv: (self.left is None) == (self.right is None)
|
|
548
|
+
"""
|
|
549
|
+
|
|
550
|
+
left: Optional[T]
|
|
551
|
+
right: Optional[T]
|
|
552
|
+
|
|
553
|
+
def setpair(self, left: Optional[T], right: Optional[T]):
|
|
554
|
+
"""post[self]: True"""
|
|
555
|
+
if (left is None) ^ (right is None):
|
|
556
|
+
raise ValueError("Populate both values or neither value in the pair")
|
|
557
|
+
self.left, self.right = left, right
|
|
558
|
+
|
|
559
|
+
messages = analyze_function(
|
|
560
|
+
FunctionInfo(MaybePair, "setpair", MaybePair.__dict__["setpair"])
|
|
561
|
+
)
|
|
562
|
+
actual, expected = check_messages(messages, state=MessageType.EXEC_ERR)
|
|
563
|
+
assert actual == expected
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def test_bad_invariant():
|
|
567
|
+
class WithBadInvariant:
|
|
568
|
+
"""
|
|
569
|
+
inv: self.item == 7
|
|
570
|
+
"""
|
|
571
|
+
|
|
572
|
+
def do_a_thing(self) -> None:
|
|
573
|
+
pass
|
|
574
|
+
|
|
575
|
+
actual, expected = check_messages(
|
|
576
|
+
analyze_class(WithBadInvariant), state=MessageType.PRE_UNSAT
|
|
577
|
+
)
|
|
578
|
+
assert actual == expected
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def test_expr_name_resolution():
|
|
582
|
+
"""
|
|
583
|
+
dataclass() generates several methods. It can be tricky to ensure
|
|
584
|
+
that invariants for these methods can resolve names in the
|
|
585
|
+
correct namespace.
|
|
586
|
+
"""
|
|
587
|
+
actual, expected = check_messages(
|
|
588
|
+
analyze_class(ReferenceHoldingClass), state=MessageType.CONFIRMED
|
|
589
|
+
)
|
|
590
|
+
assert actual == expected
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def test_inheritance_base_class_ok():
|
|
594
|
+
actual, expected = check_messages(
|
|
595
|
+
analyze_class(SmokeDetector), state=MessageType.CONFIRMED
|
|
596
|
+
)
|
|
597
|
+
assert actual == expected
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def test_super():
|
|
601
|
+
class FooDetector(SmokeDetector):
|
|
602
|
+
def signaling_alarm(self, air_samples: List[int]):
|
|
603
|
+
return super().signaling_alarm(air_samples)
|
|
604
|
+
|
|
605
|
+
actual, expected = check_messages(
|
|
606
|
+
analyze_class(FooDetector), state=MessageType.CONFIRMED
|
|
607
|
+
)
|
|
608
|
+
assert actual == expected
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def test_use_inherited_postconditions():
|
|
612
|
+
class CarbonMonoxideDetector(SmokeDetector):
|
|
613
|
+
def signaling_alarm(self, air_samples: List[int]) -> bool:
|
|
614
|
+
"""
|
|
615
|
+
post: implies(AirSample.CO2 in air_samples, _ == True)
|
|
616
|
+
"""
|
|
617
|
+
return AirSample.CO2 in air_samples # fails: does not detect smoke
|
|
618
|
+
|
|
619
|
+
actual, expected = check_messages(
|
|
620
|
+
analyze_class(CarbonMonoxideDetector), state=MessageType.POST_FAIL
|
|
621
|
+
)
|
|
622
|
+
assert actual == expected
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def test_inherited_preconditions_overridable():
|
|
626
|
+
@dataclasses.dataclass
|
|
627
|
+
class SmokeDetectorWithBattery(SmokeDetector):
|
|
628
|
+
_battery_power: int
|
|
629
|
+
|
|
630
|
+
def signaling_alarm(self, air_samples: List[int]) -> bool:
|
|
631
|
+
"""
|
|
632
|
+
pre: self._battery_power > 0 or self._is_plugged_in
|
|
633
|
+
post: self._battery_power > 0
|
|
634
|
+
"""
|
|
635
|
+
return AirSample.SMOKE in air_samples
|
|
636
|
+
|
|
637
|
+
actual, expected = check_messages(
|
|
638
|
+
analyze_class(SmokeDetectorWithBattery), state=MessageType.POST_FAIL
|
|
639
|
+
)
|
|
640
|
+
assert actual == expected
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def test_use_subclasses_of_arguments():
|
|
644
|
+
# Even though the argument below is typed as the base class, the fact
|
|
645
|
+
# that a faulty implementation exists is enough to produce a
|
|
646
|
+
# counterexample:
|
|
647
|
+
def f(foo: Cat) -> int:
|
|
648
|
+
"""post: _ == 1"""
|
|
649
|
+
return foo.size()
|
|
650
|
+
|
|
651
|
+
# Type repo doesn't load crosshair classes by default; load manually:
|
|
652
|
+
type_repo._add_class(Cat)
|
|
653
|
+
type_repo._add_class(BiggerCat)
|
|
654
|
+
check_states(f, POST_FAIL)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def test_does_not_report_with_actual_repr():
|
|
658
|
+
def f(foo: BiggerCat) -> int:
|
|
659
|
+
"""post: False"""
|
|
660
|
+
return foo.size()
|
|
661
|
+
|
|
662
|
+
(actual, expected) = check_messages(
|
|
663
|
+
analyze_function(f),
|
|
664
|
+
state=MessageType.POST_FAIL,
|
|
665
|
+
message="false when calling f(BiggerCat()) " "(which returns 2)",
|
|
666
|
+
)
|
|
667
|
+
assert expected == actual
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def test_check_parent_conditions():
|
|
671
|
+
# Ensure that conditions of parent classes are checked in children
|
|
672
|
+
# even when not overridden.
|
|
673
|
+
class Parent:
|
|
674
|
+
def size(self) -> int:
|
|
675
|
+
return 1
|
|
676
|
+
|
|
677
|
+
def amount_smaller(self, other_size: int) -> int:
|
|
678
|
+
"""
|
|
679
|
+
pre: other_size >= 1
|
|
680
|
+
post: _ >= 0
|
|
681
|
+
"""
|
|
682
|
+
return other_size - self.size()
|
|
683
|
+
|
|
684
|
+
class Child(Parent):
|
|
685
|
+
def size(self) -> int:
|
|
686
|
+
return 2
|
|
687
|
+
|
|
688
|
+
messages = analyze_class(Child)
|
|
689
|
+
actual, expected = check_messages(messages, state=MessageType.POST_FAIL)
|
|
690
|
+
assert actual == expected
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def test_final_with_concrete_proxy():
|
|
694
|
+
from typing import Final
|
|
695
|
+
|
|
696
|
+
class FinalCat:
|
|
697
|
+
legs: Final[int] = 4
|
|
698
|
+
|
|
699
|
+
def __repr__(self):
|
|
700
|
+
return f"FinalCat with {self.legs} legs"
|
|
701
|
+
|
|
702
|
+
def f(cat: FinalCat, strides: int) -> int:
|
|
703
|
+
"""
|
|
704
|
+
pre: strides > 0
|
|
705
|
+
post: __return__ >= 4
|
|
706
|
+
"""
|
|
707
|
+
return strides * cat.legs
|
|
708
|
+
|
|
709
|
+
check_states(f, CONFIRMED)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
# TODO: precondition strengthening check
|
|
713
|
+
def TODO_test_cannot_strengthen_inherited_preconditions():
|
|
714
|
+
class PowerHungrySmokeDetector(SmokeDetector):
|
|
715
|
+
_battery_power: int
|
|
716
|
+
|
|
717
|
+
def signaling_alarm(self, air_samples: List[int]) -> bool:
|
|
718
|
+
"""
|
|
719
|
+
pre: self._is_plugged_in
|
|
720
|
+
pre: self._battery_power > 0
|
|
721
|
+
"""
|
|
722
|
+
return AirSample.SMOKE in air_samples
|
|
723
|
+
|
|
724
|
+
actual, expected = check_messages(
|
|
725
|
+
analyze_class(PowerHungrySmokeDetector), state=MessageType.PRE_INVALID
|
|
726
|
+
)
|
|
727
|
+
assert actual == expected
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def test_newtype() -> None:
|
|
731
|
+
T = TypeVar("T")
|
|
732
|
+
Number = NewType("Number", int)
|
|
733
|
+
with standalone_statespace:
|
|
734
|
+
x = proxy_for_type(Number, "x", allow_subtypes=False)
|
|
735
|
+
assert isinstance(x, SymbolicInt)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
@pytest.mark.skipif(sys.version_info < (3, 12), reason="type statements added in 3.12")
|
|
739
|
+
def test_type_statement() -> None:
|
|
740
|
+
env: dict[str, Any] = {}
|
|
741
|
+
exec("type MyIntNew = int\n", env)
|
|
742
|
+
assert "MyIntNew" in env
|
|
743
|
+
MyIntNew = env["MyIntNew"]
|
|
744
|
+
with standalone_statespace:
|
|
745
|
+
x = proxy_for_type(MyIntNew, "x")
|
|
746
|
+
assert isinstance(x, SymbolicInt)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
@pytest.mark.skipif(sys.version_info < (3, 12), reason="type statements added in 3.12")
|
|
750
|
+
def test_parameterized_type_statement() -> None:
|
|
751
|
+
env: dict[str, Any] = {}
|
|
752
|
+
exec("type Pair[A, B] = tuple[B, A]\n", env)
|
|
753
|
+
assert "Pair" in env
|
|
754
|
+
Pair = env["Pair"]
|
|
755
|
+
with standalone_statespace:
|
|
756
|
+
x = proxy_for_type(Pair[int, str], "x")
|
|
757
|
+
assert isinstance(x[0], LazyIntSymbolicStr)
|
|
758
|
+
assert isinstance(x[1], SymbolicInt)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def test_container_typevar() -> None:
|
|
762
|
+
T = TypeVar("T")
|
|
763
|
+
|
|
764
|
+
def f(s: Sequence[T]) -> Dict[T, T]:
|
|
765
|
+
"""post: len(_) == len(s)"""
|
|
766
|
+
return dict(zip(s, s))
|
|
767
|
+
|
|
768
|
+
# (sequence could contain duplicate items)
|
|
769
|
+
check_states(f, POST_FAIL)
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def test_typevar_bounds_fail() -> None:
|
|
773
|
+
T = TypeVar("T")
|
|
774
|
+
|
|
775
|
+
def f(x: T) -> int:
|
|
776
|
+
"""post:True"""
|
|
777
|
+
return x + 1 # type: ignore
|
|
778
|
+
|
|
779
|
+
check_states(f, EXEC_ERR)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def test_typevar_bounds_ok() -> None:
|
|
783
|
+
B = TypeVar("B", bound=int)
|
|
784
|
+
|
|
785
|
+
def f(x: B) -> int:
|
|
786
|
+
"""post:True"""
|
|
787
|
+
return x + 1
|
|
788
|
+
|
|
789
|
+
check_states(f, CONFIRMED)
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def test_any() -> None:
|
|
793
|
+
def f(x: Any) -> bool:
|
|
794
|
+
"""post: True"""
|
|
795
|
+
return x is None
|
|
796
|
+
|
|
797
|
+
check_states(f, CONFIRMED)
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def test_meeting_class_preconditions() -> None:
|
|
801
|
+
def f() -> int:
|
|
802
|
+
"""
|
|
803
|
+
post: _ == -1
|
|
804
|
+
"""
|
|
805
|
+
pokeable = Pokeable(0)
|
|
806
|
+
pokeable.safe_pokeby(-1)
|
|
807
|
+
return pokeable.x
|
|
808
|
+
|
|
809
|
+
analyze_function(f)
|
|
810
|
+
# TODO: this doesn't test anything?
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def test_enforced_fn_preconditions() -> None:
|
|
814
|
+
def f(x: int) -> bool:
|
|
815
|
+
"""post: _ == True"""
|
|
816
|
+
return bool(fibb(x)) or True
|
|
817
|
+
|
|
818
|
+
check_states(f, EXEC_ERR)
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def test_generic_object() -> None:
|
|
822
|
+
def f(thing: object):
|
|
823
|
+
"""post: True"""
|
|
824
|
+
if isinstance(thing, SmokeDetector):
|
|
825
|
+
return thing._is_plugged_in
|
|
826
|
+
return False
|
|
827
|
+
|
|
828
|
+
check_states(f, CANNOT_CONFIRM)
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def get_natural_number() -> int:
|
|
832
|
+
"""post: _ >= 0"""
|
|
833
|
+
# crosshair: specs_complete=True
|
|
834
|
+
return 1
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
@pytest.mark.smoke
|
|
838
|
+
def test_specs_complete():
|
|
839
|
+
def f() -> int:
|
|
840
|
+
"""post: _"""
|
|
841
|
+
return get_natural_number()
|
|
842
|
+
|
|
843
|
+
(actual, expected) = check_messages(
|
|
844
|
+
analyze_function(f),
|
|
845
|
+
state=MessageType.POST_FAIL,
|
|
846
|
+
message="false when calling f() "
|
|
847
|
+
"with crosshair.patch_to_return({"
|
|
848
|
+
"crosshair.core_test.get_natural_number: [0]}) "
|
|
849
|
+
"(which returns 0)",
|
|
850
|
+
)
|
|
851
|
+
assert actual == expected
|
|
852
|
+
|
|
853
|
+
# also check that it reproduces!:
|
|
854
|
+
assert get_natural_number() == 1
|
|
855
|
+
with crosshair.patch_to_return({crosshair.core_test.get_natural_number: [0]}):
|
|
856
|
+
assert get_natural_number() == 0
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def test_access_class_method_on_symbolic_type():
|
|
860
|
+
with standalone_statespace as space:
|
|
861
|
+
person = proxy_for_type(Type[Person], "p")
|
|
862
|
+
person.a_class_method(42) # Just check that this don't explode
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def test_syntax_error() -> None:
|
|
866
|
+
def f(x: int):
|
|
867
|
+
"""pre: x && x"""
|
|
868
|
+
|
|
869
|
+
actual, expected = check_messages(analyze_function(f), state=MessageType.SYNTAX_ERR)
|
|
870
|
+
assert actual == expected
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def test_raises_ok() -> None:
|
|
874
|
+
def f() -> bool:
|
|
875
|
+
"""
|
|
876
|
+
raises: IndexError, NameError
|
|
877
|
+
post: __return__
|
|
878
|
+
"""
|
|
879
|
+
raise IndexError()
|
|
880
|
+
return True
|
|
881
|
+
|
|
882
|
+
check_states(f, CONFIRMED)
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def test_optional_can_be_none_fail() -> None:
|
|
886
|
+
def f(n: Optional[Pokeable]) -> bool:
|
|
887
|
+
"""post: _"""
|
|
888
|
+
return isinstance(n, Pokeable)
|
|
889
|
+
|
|
890
|
+
check_states(f, POST_FAIL)
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def test_implicit_heapref_conversions() -> None:
|
|
894
|
+
def f(foo: List[List]) -> None:
|
|
895
|
+
"""
|
|
896
|
+
pre: len(foo) > 0
|
|
897
|
+
post: True
|
|
898
|
+
"""
|
|
899
|
+
foo[0].append(42)
|
|
900
|
+
|
|
901
|
+
check_states(f, CONFIRMED)
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
def test_nonuniform_list_types_1() -> None:
|
|
905
|
+
def f(a: List[object], b: List[int]) -> List[object]:
|
|
906
|
+
"""
|
|
907
|
+
pre: len(b) == 5 # constraint for performance
|
|
908
|
+
post: b[0] not in _
|
|
909
|
+
"""
|
|
910
|
+
ret = a + b[1:] # type: ignore
|
|
911
|
+
return ret
|
|
912
|
+
|
|
913
|
+
check_states(f, POST_FAIL)
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def test_nonuniform_list_types_2() -> None:
|
|
917
|
+
def f(a: List[object], b: List[int]) -> List[object]:
|
|
918
|
+
"""
|
|
919
|
+
pre: len(b) == 5 # constraint for performance
|
|
920
|
+
post: b[-1] not in _
|
|
921
|
+
"""
|
|
922
|
+
return a + b[:-1] # type: ignore
|
|
923
|
+
|
|
924
|
+
check_states(f, POST_FAIL)
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def test_varargs_fail() -> None:
|
|
928
|
+
def f(x: int, *a: str, **kw: bool) -> int:
|
|
929
|
+
"""post: _ > x"""
|
|
930
|
+
return x + len(a) + (42 if kw else 0)
|
|
931
|
+
|
|
932
|
+
check_states(f, POST_FAIL)
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def test_varargs_ok() -> None:
|
|
936
|
+
def f(x: int, *a: str, **kw: bool) -> int:
|
|
937
|
+
"""post: _ >= x"""
|
|
938
|
+
return x + len(a) + (42 if kw else 0)
|
|
939
|
+
|
|
940
|
+
check_states(f, CANNOT_CONFIRM)
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def test_recursive_fn_fail() -> None:
|
|
944
|
+
check_states(fibb, POST_FAIL)
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def test_recursive_fn_ok() -> None:
|
|
948
|
+
check_states(recursive_example, CONFIRMED)
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def test_recursive_postcondition_ok() -> None:
|
|
952
|
+
def f(x: int) -> int:
|
|
953
|
+
"""post: _ == f(-x)"""
|
|
954
|
+
return x * x
|
|
955
|
+
|
|
956
|
+
check_states(f, CONFIRMED)
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
def test_reentrant_precondition() -> None:
|
|
960
|
+
# Really, we're just ensuring that we don't stack-overflow here.
|
|
961
|
+
check_states(reentrant_precondition, CONFIRMED)
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def test_recursive_postcondition_enforcement_suspension() -> None:
|
|
965
|
+
messages = analyze_class(Measurer)
|
|
966
|
+
actual, expected = check_messages(messages, state=MessageType.POST_FAIL)
|
|
967
|
+
assert actual == expected
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def test_short_circuiting() -> None:
|
|
971
|
+
# Some operations are hard to deal with symbolically, like hashes.
|
|
972
|
+
# CrossHair will sometimes "short-circuit" functions, in hopes that the
|
|
973
|
+
# function body isn't required to prove the postcondition.
|
|
974
|
+
# This is an example of such a case.
|
|
975
|
+
def f(x: str) -> int:
|
|
976
|
+
"""post: _ == 0"""
|
|
977
|
+
a = hash(x)
|
|
978
|
+
b = 7
|
|
979
|
+
# This is zero no matter what the hashes are:
|
|
980
|
+
return (a + b) - (b + a)
|
|
981
|
+
|
|
982
|
+
check_states(f, CONFIRMED)
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def test_error_message_in_unrelated_method() -> None:
|
|
986
|
+
messages = analyze_class(OverloadedContainer)
|
|
987
|
+
line = ShippingContainer.total_weight.__code__.co_firstlineno + 1
|
|
988
|
+
actual, expected = check_messages(
|
|
989
|
+
messages,
|
|
990
|
+
state=MessageType.POST_FAIL,
|
|
991
|
+
message="false when calling total_weight(OverloadedContainer()) (which returns 13)",
|
|
992
|
+
line=line,
|
|
993
|
+
)
|
|
994
|
+
assert actual == expected
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def test_error_message_has_unmodified_args() -> None:
|
|
998
|
+
def f(foo: List[Pokeable]) -> None:
|
|
999
|
+
"""
|
|
1000
|
+
pre: len(foo) == 1
|
|
1001
|
+
pre: foo[0].x == 10
|
|
1002
|
+
post[foo]: foo[0].x == 12
|
|
1003
|
+
"""
|
|
1004
|
+
foo[0].poke()
|
|
1005
|
+
|
|
1006
|
+
actual, expected = check_messages(
|
|
1007
|
+
analyze_function(f),
|
|
1008
|
+
state=MessageType.POST_FAIL,
|
|
1009
|
+
message="false when calling f([Pokeable(x=10)])",
|
|
1010
|
+
)
|
|
1011
|
+
assert actual == expected
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
# TODO: List[List] involves no HeapRefs
|
|
1015
|
+
def TODO_test_potential_circular_references() -> None:
|
|
1016
|
+
# TODO?: potential aliasing of input argument data?
|
|
1017
|
+
def f(foo: List[List], thing: object) -> None:
|
|
1018
|
+
"""
|
|
1019
|
+
pre: len(foo) == 2
|
|
1020
|
+
pre: len(foo[0]) == 1
|
|
1021
|
+
pre: len(foo[1]) == 1
|
|
1022
|
+
post: len(foo[1]) == 1
|
|
1023
|
+
"""
|
|
1024
|
+
foo[0].append(object()) # TODO: using 42 yields a z3 sort error
|
|
1025
|
+
|
|
1026
|
+
check_states(f, CONFIRMED)
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
def test_nonatomic_comparison() -> None:
|
|
1030
|
+
def f(x: int, ls: List[str]) -> bool:
|
|
1031
|
+
"""post: not _"""
|
|
1032
|
+
return ls == x
|
|
1033
|
+
|
|
1034
|
+
check_states(f, CONFIRMED)
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def test_difficult_equality() -> None:
|
|
1038
|
+
def f(x: Dict[FrozenSet[float], int]) -> bool:
|
|
1039
|
+
"""post: not _"""
|
|
1040
|
+
return x == {frozenset({10.0}): 1}
|
|
1041
|
+
|
|
1042
|
+
check_states(f, POST_FAIL)
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def test_old_works_in_invariants() -> None:
|
|
1046
|
+
@dataclasses.dataclass
|
|
1047
|
+
class FrozenApples:
|
|
1048
|
+
"""inv: self.count == __old__.self.count"""
|
|
1049
|
+
|
|
1050
|
+
count: int
|
|
1051
|
+
|
|
1052
|
+
def add_one(self):
|
|
1053
|
+
self.count += 1
|
|
1054
|
+
|
|
1055
|
+
messages = analyze_class(FrozenApples)
|
|
1056
|
+
actual, expected = check_messages(messages, state=MessageType.POST_FAIL)
|
|
1057
|
+
assert actual == expected
|
|
1058
|
+
|
|
1059
|
+
# Also confirm we can create one as an argument:
|
|
1060
|
+
def f(a: FrozenApples) -> int:
|
|
1061
|
+
"""post: True"""
|
|
1062
|
+
return 0
|
|
1063
|
+
|
|
1064
|
+
check_states(f, CONFIRMED)
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def test_class_patching_is_undone() -> None:
|
|
1068
|
+
# CrossHair does a lot of monkey matching of classes
|
|
1069
|
+
# with contracts. Ensure that gets undone.
|
|
1070
|
+
original_container = ShippingContainer.__dict__.copy()
|
|
1071
|
+
original_overloaded = OverloadedContainer.__dict__.copy()
|
|
1072
|
+
run_checkables(analyze_class(OverloadedContainer))
|
|
1073
|
+
for k, v in original_container.items():
|
|
1074
|
+
assert ShippingContainer.__dict__[k] is v
|
|
1075
|
+
for k, v in original_overloaded.items():
|
|
1076
|
+
assert OverloadedContainer.__dict__[k] is v
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
def test_fallback_when_smt_values_out_themselves() -> None:
|
|
1080
|
+
def f(items: List[str]) -> str:
|
|
1081
|
+
"""post: True"""
|
|
1082
|
+
return ",".join(items)
|
|
1083
|
+
|
|
1084
|
+
check_states(f, CANNOT_CONFIRM)
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
def test_unrelated_regex() -> None:
|
|
1088
|
+
def f(s: str) -> bool:
|
|
1089
|
+
"""post: True"""
|
|
1090
|
+
return bool(re.match(r"(\d+)", s))
|
|
1091
|
+
|
|
1092
|
+
check_states(f, CANNOT_CONFIRM)
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
if sys.version_info >= (3, 9):
|
|
1096
|
+
|
|
1097
|
+
def test_new_style_type_hints():
|
|
1098
|
+
def f(ls: list[int]) -> List[int]:
|
|
1099
|
+
"""
|
|
1100
|
+
pre: len(ls) == 1
|
|
1101
|
+
post: _[0] != 'a'
|
|
1102
|
+
"""
|
|
1103
|
+
return ls
|
|
1104
|
+
|
|
1105
|
+
check_states(f, CONFIRMED)
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def test_nondeterministic_detected_via_condition() -> None:
|
|
1109
|
+
_GLOBAL_THING = [42]
|
|
1110
|
+
|
|
1111
|
+
def f(i: int) -> int:
|
|
1112
|
+
"""post: True"""
|
|
1113
|
+
_GLOBAL_THING[0] += 1
|
|
1114
|
+
if i > _GLOBAL_THING[0]:
|
|
1115
|
+
pass
|
|
1116
|
+
return True
|
|
1117
|
+
|
|
1118
|
+
actual, expected = check_exec_err(f, "NotDeterministic")
|
|
1119
|
+
assert actual == expected
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def test_nondeterministic_detected_in_detached_path() -> None:
|
|
1123
|
+
_GLOBAL_THING = [True]
|
|
1124
|
+
|
|
1125
|
+
def f(i: int) -> int:
|
|
1126
|
+
"""post: True"""
|
|
1127
|
+
_GLOBAL_THING[0] = not _GLOBAL_THING[0]
|
|
1128
|
+
if _GLOBAL_THING[0]:
|
|
1129
|
+
raise Exception
|
|
1130
|
+
else:
|
|
1131
|
+
return -i if i < 0 else i
|
|
1132
|
+
|
|
1133
|
+
actual, expected = check_exec_err(f, "NotDeterministic")
|
|
1134
|
+
assert actual == expected
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
if icontract:
|
|
1138
|
+
|
|
1139
|
+
def test_icontract_basic():
|
|
1140
|
+
@icontract.ensure(lambda result, x: result > x)
|
|
1141
|
+
def some_func(x: int, y: int = 5) -> int:
|
|
1142
|
+
return x - y
|
|
1143
|
+
|
|
1144
|
+
check_states(some_func, POST_FAIL)
|
|
1145
|
+
|
|
1146
|
+
def test_icontract_snapshots():
|
|
1147
|
+
messages = analyze_function(
|
|
1148
|
+
icontract_appender,
|
|
1149
|
+
DEFAULT_OPTIONS,
|
|
1150
|
+
)
|
|
1151
|
+
line = icontract_appender.__wrapped__.__code__.co_firstlineno + 1
|
|
1152
|
+
actual, expected = check_messages(
|
|
1153
|
+
messages, state=MessageType.POST_FAIL, line=line, column=0
|
|
1154
|
+
)
|
|
1155
|
+
assert actual == expected
|
|
1156
|
+
|
|
1157
|
+
def test_icontract_weaken():
|
|
1158
|
+
@icontract.require(lambda x: x in (2, 3))
|
|
1159
|
+
@icontract.ensure(lambda: True)
|
|
1160
|
+
def trynum(x: int):
|
|
1161
|
+
IcontractB().weakenedfunc(x)
|
|
1162
|
+
|
|
1163
|
+
check_states(trynum, CONFIRMED)
|
|
1164
|
+
|
|
1165
|
+
@pytest.mark.skip(
|
|
1166
|
+
reason="Temporary disable: RecursionError on 3.11.13 - icontract's _IN_PROGRESS + CrossHair laziness cross-test leaks"
|
|
1167
|
+
)
|
|
1168
|
+
def test_icontract_class():
|
|
1169
|
+
messages = run_checkables(
|
|
1170
|
+
analyze_class(
|
|
1171
|
+
IcontractB,
|
|
1172
|
+
# TODO: why is this required?
|
|
1173
|
+
DEFAULT_OPTIONS.overlay(analysis_kind=[AnalysisKind.icontract]),
|
|
1174
|
+
)
|
|
1175
|
+
)
|
|
1176
|
+
messages = {
|
|
1177
|
+
(m.state, m.line, m.message)
|
|
1178
|
+
for m in messages
|
|
1179
|
+
if m.state != MessageType.CONFIRMED
|
|
1180
|
+
}
|
|
1181
|
+
line_gt0 = IcontractB.break_parent_invariant.__wrapped__.__code__.co_firstlineno
|
|
1182
|
+
line_lt100 = IcontractB.break_my_invariant.__wrapped__.__code__.co_firstlineno
|
|
1183
|
+
assert messages == {
|
|
1184
|
+
(
|
|
1185
|
+
MessageType.POST_FAIL,
|
|
1186
|
+
line_gt0,
|
|
1187
|
+
'"@icontract.invariant(lambda self: self.x > 0)" yields false '
|
|
1188
|
+
"when calling break_parent_invariant(IcontractB())",
|
|
1189
|
+
),
|
|
1190
|
+
(
|
|
1191
|
+
MessageType.POST_FAIL,
|
|
1192
|
+
line_lt100,
|
|
1193
|
+
'"@icontract.invariant(lambda self: self.x < 100)" yields false '
|
|
1194
|
+
"when calling break_my_invariant(IcontractB())",
|
|
1195
|
+
),
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
def test_icontract_nesting():
|
|
1199
|
+
@icontract.require(lambda name: name.startswith("a"))
|
|
1200
|
+
def innerfn(name: str):
|
|
1201
|
+
pass
|
|
1202
|
+
|
|
1203
|
+
@icontract.ensure(lambda: True)
|
|
1204
|
+
@icontract.require(lambda name: len(name) > 0)
|
|
1205
|
+
def outerfn(name: str):
|
|
1206
|
+
innerfn("00" + name)
|
|
1207
|
+
|
|
1208
|
+
actual, expected = check_exec_err(
|
|
1209
|
+
outerfn,
|
|
1210
|
+
message_prefix="PreconditionFailed",
|
|
1211
|
+
)
|
|
1212
|
+
assert actual == expected
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
def test_asserts():
|
|
1216
|
+
messages = analyze_function(
|
|
1217
|
+
remove_smallest_with_asserts,
|
|
1218
|
+
DEFAULT_OPTIONS.overlay(
|
|
1219
|
+
analysis_kind=[AnalysisKind.asserts],
|
|
1220
|
+
),
|
|
1221
|
+
)
|
|
1222
|
+
line = remove_smallest_with_asserts.__code__.co_firstlineno + 4
|
|
1223
|
+
expected, actual = check_messages(
|
|
1224
|
+
messages, state=MessageType.EXEC_ERR, line=line, column=0
|
|
1225
|
+
)
|
|
1226
|
+
assert expected == actual
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
def test_unpickable_args() -> None:
|
|
1230
|
+
from threading import RLock # RLock objects aren't copyable
|
|
1231
|
+
|
|
1232
|
+
@dataclasses.dataclass
|
|
1233
|
+
class WithUnpickleableArg:
|
|
1234
|
+
x: int
|
|
1235
|
+
lock: RLock
|
|
1236
|
+
|
|
1237
|
+
def dothing(foo: WithUnpickleableArg) -> int:
|
|
1238
|
+
"""
|
|
1239
|
+
post: __return__ != 42
|
|
1240
|
+
"""
|
|
1241
|
+
return foo.x
|
|
1242
|
+
|
|
1243
|
+
check_states(dothing, POST_FAIL)
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
def test_kwargs(space):
|
|
1247
|
+
def callme(lu=3, **kw):
|
|
1248
|
+
return 42
|
|
1249
|
+
|
|
1250
|
+
kwargs = proxy_for_type(Dict[str, int], "kwargs")
|
|
1251
|
+
with ResumedTracing():
|
|
1252
|
+
space.add(kwargs.__len__() == 1)
|
|
1253
|
+
assert callme(**kwargs) == 42 # (this is a CALL_FUNCTION_EX opcode)
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
@pytest.mark.smoke
|
|
1257
|
+
def test_deep_realize(space):
|
|
1258
|
+
x = proxy_for_type(int, "x")
|
|
1259
|
+
with ResumedTracing():
|
|
1260
|
+
space.add(x == 4)
|
|
1261
|
+
|
|
1262
|
+
@dataclasses.dataclass
|
|
1263
|
+
class Woo:
|
|
1264
|
+
stuff: Dict[str, int]
|
|
1265
|
+
|
|
1266
|
+
woo = Woo({"": x})
|
|
1267
|
+
assert type(woo.stuff[""]) is not int
|
|
1268
|
+
realized = deep_realize(woo)
|
|
1269
|
+
assert type(realized.stuff[""]) is int
|
|
1270
|
+
assert realized.stuff[""] == 4
|
|
1271
|
+
|
|
1272
|
+
|
|
1273
|
+
@pytest.mark.parametrize(
|
|
1274
|
+
"o", (4, "foo", 23.1, None, (12,), frozenset({1, 2}), ((), (4,)))
|
|
1275
|
+
)
|
|
1276
|
+
def test_is_deeply_immutable(o):
|
|
1277
|
+
with standalone_statespace:
|
|
1278
|
+
assert is_deeply_immutable(o)
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
@pytest.mark.parametrize("o", ({}, {1: 2}, [], (3, []), ("foo", (3, []))))
|
|
1282
|
+
def test_is_not_deeply_immutable(o):
|
|
1283
|
+
with standalone_statespace:
|
|
1284
|
+
assert not is_deeply_immutable(o)
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
def test_crosshair_modules_can_be_reloaded():
|
|
1288
|
+
importlib.reload(core_and_libs)
|
|
1289
|
+
|
|
1290
|
+
|
|
1291
|
+
def profile():
|
|
1292
|
+
# This is a scratch area to run quick profiles.
|
|
1293
|
+
def f(x: int) -> int:
|
|
1294
|
+
"""
|
|
1295
|
+
post: _ != 123456
|
|
1296
|
+
"""
|
|
1297
|
+
return hash(x)
|
|
1298
|
+
|
|
1299
|
+
check_states(f, AnalysisOptionSet(max_iterations=20)) == {
|
|
1300
|
+
MessageType.CANNOT_CONFIRM
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
if __name__ == "__main__":
|
|
1305
|
+
if ("-v" in sys.argv) or ("--verbose" in sys.argv):
|
|
1306
|
+
set_debug(True)
|
|
1307
|
+
if "-p" in sys.argv:
|
|
1308
|
+
import time
|
|
1309
|
+
|
|
1310
|
+
t0 = time.time()
|
|
1311
|
+
profile()
|
|
1312
|
+
print("check seconds:", time.time() - t0)
|
|
1313
|
+
elif "-t" in sys.argv:
|
|
1314
|
+
import cProfile
|
|
1315
|
+
|
|
1316
|
+
cProfile.run("profile()", "out.pprof")
|