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/enforce.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import copy
|
|
3
|
+
import functools
|
|
4
|
+
import os
|
|
5
|
+
from types import FrameType
|
|
6
|
+
from typing import Callable, Dict, Mapping, Optional, Set, Tuple
|
|
7
|
+
|
|
8
|
+
from crosshair.condition_parser import (
|
|
9
|
+
ConditionParser,
|
|
10
|
+
Conditions,
|
|
11
|
+
NoEnforce,
|
|
12
|
+
fn_globals,
|
|
13
|
+
get_current_parser,
|
|
14
|
+
)
|
|
15
|
+
from crosshair.fnutil import FunctionInfo
|
|
16
|
+
from crosshair.statespace import prefer_true
|
|
17
|
+
from crosshair.tracers import COMPOSITE_TRACER, NoTracing, ResumedTracing, TracingModule
|
|
18
|
+
from crosshair.util import AttributeHolder
|
|
19
|
+
|
|
20
|
+
# [Pre|Post]conditionFailed exceptions extend BaseException just to reduce the
|
|
21
|
+
# possibility that end-user code accidentally handles them.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PreconditionFailed(BaseException):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PostconditionFailed(BaseException):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def WithEnforcement(fn: Callable) -> Callable:
|
|
33
|
+
"""
|
|
34
|
+
Ensure conditions are enforced on the given callable.
|
|
35
|
+
|
|
36
|
+
Enforcement is normally disabled when calling from some internal files, for
|
|
37
|
+
performance reasons. Use WithEnforcement to ensure it is enabled anywhere.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# This local function has a special name that we look for while tracing
|
|
41
|
+
# (see the wants_codeobj method below):
|
|
42
|
+
def _crosshair_with_enforcement(*a, **kw):
|
|
43
|
+
return fn(*a, **kw)
|
|
44
|
+
|
|
45
|
+
return _crosshair_with_enforcement
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def manual_constructor(typ: type):
|
|
49
|
+
def manually_construct(*a, **kw):
|
|
50
|
+
obj = WithEnforcement(typ.__new__)(typ, *a, **kw) # object.__new__(typ)
|
|
51
|
+
with NoTracing():
|
|
52
|
+
# Python does not invoke __init__ if __new__ returns an object of another type
|
|
53
|
+
# https://docs.python.org/3/reference/datamodel.html#object.__new__
|
|
54
|
+
if isinstance(obj, typ):
|
|
55
|
+
with ResumedTracing():
|
|
56
|
+
WithEnforcement(obj.__init__)(*a, **kw) # type: ignore
|
|
57
|
+
return obj
|
|
58
|
+
|
|
59
|
+
return manually_construct
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_MISSING = object()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def EnforcementWrapper(
|
|
66
|
+
fn: Callable,
|
|
67
|
+
conditions: Conditions,
|
|
68
|
+
enforced: "EnforcedConditions",
|
|
69
|
+
first_arg: object,
|
|
70
|
+
) -> Callable:
|
|
71
|
+
signature = conditions.sig
|
|
72
|
+
|
|
73
|
+
def _crosshair_wrapper(*a, **kw):
|
|
74
|
+
with NoTracing():
|
|
75
|
+
fns_enforcing = enforced.fns_enforcing
|
|
76
|
+
if fns_enforcing is None or fn in fns_enforcing:
|
|
77
|
+
with ResumedTracing():
|
|
78
|
+
return fn(*a, **kw)
|
|
79
|
+
with enforced.currently_enforcing(fn):
|
|
80
|
+
# debug("Calling enforcement wrapper ", fn.__name__, signature)
|
|
81
|
+
bound_args = signature.bind(*a, **kw)
|
|
82
|
+
bound_args.apply_defaults()
|
|
83
|
+
old = {}
|
|
84
|
+
mutable_args = conditions.mutable_args
|
|
85
|
+
mutable_args_remaining = (
|
|
86
|
+
set(mutable_args) if mutable_args is not None else set()
|
|
87
|
+
)
|
|
88
|
+
for argname, argval in bound_args.arguments.items():
|
|
89
|
+
try:
|
|
90
|
+
# TODO: reduce the type realizations when argval is a SymbolicObject
|
|
91
|
+
old[argname] = copy.copy(argval)
|
|
92
|
+
except TypeError as exc: # for uncopyables
|
|
93
|
+
pass
|
|
94
|
+
if argname in mutable_args_remaining:
|
|
95
|
+
mutable_args_remaining.remove(argname)
|
|
96
|
+
if mutable_args_remaining:
|
|
97
|
+
raise PostconditionFailed(
|
|
98
|
+
'Unrecognized mutable argument(s) in postcondition: "{}"'.format(
|
|
99
|
+
",".join(mutable_args_remaining)
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
for precondition in conditions.pre:
|
|
103
|
+
# debug(' precondition eval ', precondition.expr_source)
|
|
104
|
+
# TODO: is fn_globals required here?
|
|
105
|
+
with ResumedTracing():
|
|
106
|
+
if not precondition.evaluate(bound_args.arguments):
|
|
107
|
+
raise PreconditionFailed(
|
|
108
|
+
f'Precondition "{precondition.expr_source}" was not satisfied '
|
|
109
|
+
f'before calling "{fn.__name__}"'
|
|
110
|
+
)
|
|
111
|
+
with ResumedTracing():
|
|
112
|
+
ret = fn(*a, **kw)
|
|
113
|
+
with enforced.currently_enforcing(fn):
|
|
114
|
+
if fn.__name__ in ("__init__", "__new__"):
|
|
115
|
+
old["self"] = a[0]
|
|
116
|
+
lcls = {
|
|
117
|
+
**bound_args.arguments,
|
|
118
|
+
"__return__": ret,
|
|
119
|
+
"_": ret,
|
|
120
|
+
"__old__": AttributeHolder(old),
|
|
121
|
+
}
|
|
122
|
+
args = {**fn_globals(fn), **lcls}
|
|
123
|
+
for postcondition in conditions.post:
|
|
124
|
+
# debug('Checking postcondition ', postcondition.expr_source, ' on ', fn)
|
|
125
|
+
if not postcondition.evaluate:
|
|
126
|
+
continue
|
|
127
|
+
with ResumedTracing():
|
|
128
|
+
postcondition_ok = postcondition.evaluate(args)
|
|
129
|
+
if not prefer_true(postcondition_ok):
|
|
130
|
+
raise PostconditionFailed(
|
|
131
|
+
"Postcondition failed at {}:{}".format(
|
|
132
|
+
postcondition.filename, postcondition.line
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
# debug("Completed enforcement wrapper ", fn)
|
|
136
|
+
return ret
|
|
137
|
+
|
|
138
|
+
functools.update_wrapper(_crosshair_wrapper, fn)
|
|
139
|
+
return _crosshair_wrapper
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
_MISSING = object()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
_FILE_SUFFIXES_WITHOUT_ENFORCEMENT: Tuple[str, ...] = (
|
|
146
|
+
"/ast.py",
|
|
147
|
+
"/crosshair/libimpl/builtinslib.py",
|
|
148
|
+
"/crosshair/core.py",
|
|
149
|
+
"/crosshair/condition_parser.py",
|
|
150
|
+
"/crosshair/enforce.py",
|
|
151
|
+
"/crosshair/util.py",
|
|
152
|
+
"/crosshair/fnutil.py",
|
|
153
|
+
"/crosshair/statespace.py",
|
|
154
|
+
"/crosshair/tracers.py",
|
|
155
|
+
"/z3.py",
|
|
156
|
+
"/z3core.py",
|
|
157
|
+
"/z3printer.py",
|
|
158
|
+
"/z3types.py",
|
|
159
|
+
"/copy.py",
|
|
160
|
+
"/inspect.py",
|
|
161
|
+
"/re.py",
|
|
162
|
+
"/copyreg.py",
|
|
163
|
+
"/sre_parse.py",
|
|
164
|
+
"/sre_compile.py",
|
|
165
|
+
"/traceback.py",
|
|
166
|
+
"/contextlib.py",
|
|
167
|
+
"/linecache.py",
|
|
168
|
+
"/collections/__init__.py",
|
|
169
|
+
"/enum.py",
|
|
170
|
+
"/typing.py",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if os.name == "nt":
|
|
174
|
+
# Hacky platform-independence for performance reasons.
|
|
175
|
+
# (not sure whether there are landmines here?)
|
|
176
|
+
_FILE_SUFFIXES_WITHOUT_ENFORCEMENT = tuple(
|
|
177
|
+
p.replace("/", "\\") for p in _FILE_SUFFIXES_WITHOUT_ENFORCEMENT
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class EnforcedConditions(TracingModule):
|
|
182
|
+
def __init__(
|
|
183
|
+
self,
|
|
184
|
+
condition_parser: Optional[ConditionParser] = None,
|
|
185
|
+
interceptor=lambda x: x,
|
|
186
|
+
):
|
|
187
|
+
super().__init__()
|
|
188
|
+
self.condition_parser = (
|
|
189
|
+
get_current_parser() if condition_parser is None else condition_parser
|
|
190
|
+
)
|
|
191
|
+
self.interceptor = interceptor
|
|
192
|
+
self.fns_enforcing: Optional[Set[Callable]] = None
|
|
193
|
+
self.codeobj_cache: Dict[object, bool] = {}
|
|
194
|
+
|
|
195
|
+
@contextlib.contextmanager
|
|
196
|
+
def currently_enforcing(self, fn: Callable):
|
|
197
|
+
if self.fns_enforcing is None:
|
|
198
|
+
yield None
|
|
199
|
+
else:
|
|
200
|
+
self.fns_enforcing.add(fn)
|
|
201
|
+
try:
|
|
202
|
+
yield None
|
|
203
|
+
finally:
|
|
204
|
+
self.fns_enforcing.remove(fn)
|
|
205
|
+
|
|
206
|
+
# TODO: replace this with PushedModule(EnforcedConditions)?
|
|
207
|
+
@contextlib.contextmanager
|
|
208
|
+
def enabled_enforcement(self):
|
|
209
|
+
prev = self.fns_enforcing
|
|
210
|
+
assert prev is None
|
|
211
|
+
self.fns_enforcing = set()
|
|
212
|
+
COMPOSITE_TRACER.push_module(self)
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
yield None
|
|
216
|
+
finally:
|
|
217
|
+
self.fns_enforcing = prev
|
|
218
|
+
COMPOSITE_TRACER.pop_config(self)
|
|
219
|
+
|
|
220
|
+
def wants_codeobj(self, codeobj) -> bool:
|
|
221
|
+
name = codeobj.co_name
|
|
222
|
+
if name == "_crosshair_with_enforcement":
|
|
223
|
+
return True
|
|
224
|
+
fname = codeobj.co_filename
|
|
225
|
+
if fname.endswith(_FILE_SUFFIXES_WITHOUT_ENFORCEMENT):
|
|
226
|
+
return False
|
|
227
|
+
if name == "_crosshair_wrapper":
|
|
228
|
+
return False
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
def cached_wants_codeobj(self, codeobj) -> bool:
|
|
232
|
+
cache = self.codeobj_cache
|
|
233
|
+
cachedval = cache.get(codeobj)
|
|
234
|
+
if cachedval is None:
|
|
235
|
+
cachedval = self.wants_codeobj(codeobj)
|
|
236
|
+
cache[codeobj] = cachedval
|
|
237
|
+
return cachedval
|
|
238
|
+
|
|
239
|
+
def trace_call(
|
|
240
|
+
self,
|
|
241
|
+
frame: FrameType,
|
|
242
|
+
fn: Callable,
|
|
243
|
+
binding_target: object,
|
|
244
|
+
) -> Optional[Callable]:
|
|
245
|
+
caller_code = frame.f_code
|
|
246
|
+
if not self.cached_wants_codeobj(caller_code):
|
|
247
|
+
return None
|
|
248
|
+
try:
|
|
249
|
+
target_name = object.__getattribute__(fn, "__name__")
|
|
250
|
+
except AttributeError:
|
|
251
|
+
target_name = ""
|
|
252
|
+
if target_name.endswith((">", "_crosshair_wrapper")):
|
|
253
|
+
return None
|
|
254
|
+
if isinstance(fn, NoEnforce):
|
|
255
|
+
return fn.fn
|
|
256
|
+
if isinstance(fn, type) and fn not in (super, type):
|
|
257
|
+
return manual_constructor(fn)
|
|
258
|
+
|
|
259
|
+
parser = self.condition_parser
|
|
260
|
+
conditions = None
|
|
261
|
+
if binding_target is None:
|
|
262
|
+
conditions = parser.get_fn_conditions(FunctionInfo(None, "", fn)) # type: ignore
|
|
263
|
+
else:
|
|
264
|
+
# Method call.
|
|
265
|
+
# We normally expect to look up contracts on `type(binding_target)`, but
|
|
266
|
+
# if it's a `@classmethod`, we'll find it directly on `binding_target`.
|
|
267
|
+
# TODO: test contracts on metaclass methods
|
|
268
|
+
if isinstance(binding_target, type):
|
|
269
|
+
instance_methods = parser.get_class_conditions(binding_target).methods
|
|
270
|
+
conditions = instance_methods.get(target_name)
|
|
271
|
+
if conditions is None or not conditions.has_any():
|
|
272
|
+
type_methods = parser.get_class_conditions(type(binding_target)).methods
|
|
273
|
+
conditions = type_methods.get(target_name)
|
|
274
|
+
|
|
275
|
+
if conditions is not None and not conditions.has_any():
|
|
276
|
+
conditions = None
|
|
277
|
+
if conditions is None:
|
|
278
|
+
return None
|
|
279
|
+
# debug("Enforcing conditions on", fn, " type(binding)=", type(binding_target))
|
|
280
|
+
fn = self.interceptor(fn) # conditions.fn) # type: ignore
|
|
281
|
+
wrapper = EnforcementWrapper(fn, conditions, self, binding_target) # type: ignore
|
|
282
|
+
return wrapper
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import sys
|
|
3
|
+
from contextlib import ExitStack
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from crosshair.condition_parser import Pep316Parser
|
|
8
|
+
from crosshair.enforce import (
|
|
9
|
+
EnforcedConditions,
|
|
10
|
+
PostconditionFailed,
|
|
11
|
+
PreconditionFailed,
|
|
12
|
+
manual_constructor,
|
|
13
|
+
)
|
|
14
|
+
from crosshair.tracers import COMPOSITE_TRACER
|
|
15
|
+
from crosshair.util import set_debug
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def foo(x: int) -> int:
|
|
19
|
+
"""
|
|
20
|
+
pre: 0 <= x <= 100
|
|
21
|
+
post: _ > x
|
|
22
|
+
"""
|
|
23
|
+
return x * 2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Pokeable:
|
|
27
|
+
"""
|
|
28
|
+
inv: self.x >= 0
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
x: int = 1
|
|
32
|
+
|
|
33
|
+
def poke(self) -> None:
|
|
34
|
+
self.x += 1
|
|
35
|
+
|
|
36
|
+
def pokeby(self, amount: int) -> None:
|
|
37
|
+
"""
|
|
38
|
+
pre: amount >= 0
|
|
39
|
+
"""
|
|
40
|
+
self.x += amount
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def same_thing(thing: object) -> object:
|
|
44
|
+
"""post: __old__.thing == _"""
|
|
45
|
+
# If `thing` isn't copyable, it won't be available in `__old__`.
|
|
46
|
+
# In this case, enforcement will fail with an AttributeError.
|
|
47
|
+
return thing
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Enforcement(ExitStack):
|
|
51
|
+
def __enter__(self):
|
|
52
|
+
super().__enter__()
|
|
53
|
+
enforced_conditions = EnforcedConditions(Pep316Parser())
|
|
54
|
+
self.enter_context(COMPOSITE_TRACER)
|
|
55
|
+
self.enter_context(enforced_conditions.enabled_enforcement())
|
|
56
|
+
COMPOSITE_TRACER.trace_caller()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestCore:
|
|
60
|
+
def test_enforce_conditions(self) -> None:
|
|
61
|
+
assert foo(-1) == -2 # unchecked
|
|
62
|
+
with Enforcement():
|
|
63
|
+
assert foo(50) == 100
|
|
64
|
+
with pytest.raises(PreconditionFailed):
|
|
65
|
+
foo(-1)
|
|
66
|
+
with pytest.raises(PostconditionFailed):
|
|
67
|
+
foo(0)
|
|
68
|
+
|
|
69
|
+
def test_class_enforce(self) -> None:
|
|
70
|
+
Pokeable().pokeby(-1) # no exception (yet!)
|
|
71
|
+
with Enforcement():
|
|
72
|
+
Pokeable().poke()
|
|
73
|
+
with pytest.raises(PreconditionFailed):
|
|
74
|
+
Pokeable().pokeby(-1)
|
|
75
|
+
|
|
76
|
+
def test_enforce_on_uncopyable_value(self) -> None:
|
|
77
|
+
class NotCopyable:
|
|
78
|
+
def __copy__(self):
|
|
79
|
+
raise TypeError("not copyable")
|
|
80
|
+
|
|
81
|
+
not_copyable = NotCopyable()
|
|
82
|
+
with Enforcement():
|
|
83
|
+
with pytest.raises(AttributeError):
|
|
84
|
+
same_thing(not_copyable)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class BaseFooable:
|
|
88
|
+
def foo(self, x: int):
|
|
89
|
+
"""pre: x > 100"""
|
|
90
|
+
|
|
91
|
+
def foo_only_in_super(self, x: int):
|
|
92
|
+
"""pre: x > 100"""
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def class_foo(cls, x: int):
|
|
96
|
+
"""pre: x > 100"""
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def static_foo(x: int):
|
|
100
|
+
"""pre: x > 100"""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class DerivedFooable(BaseFooable):
|
|
104
|
+
def foo(self, x: int):
|
|
105
|
+
"""pre: x > 0"""
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def class_foo(cls, x: int):
|
|
109
|
+
"""pre: x > 0"""
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def static_foo(x: int):
|
|
113
|
+
"""pre: x > 0"""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestTrickyCases:
|
|
117
|
+
def test_attrs_restored_properly(self) -> None:
|
|
118
|
+
orig_class_dict = DerivedFooable.__dict__.copy()
|
|
119
|
+
with Enforcement():
|
|
120
|
+
pass
|
|
121
|
+
for k, v in orig_class_dict.items():
|
|
122
|
+
assert (
|
|
123
|
+
DerivedFooable.__dict__[k] is v
|
|
124
|
+
), f'member "{k}" changed afer encforcement'
|
|
125
|
+
|
|
126
|
+
def test_enforcement_of_class_methods(self) -> None:
|
|
127
|
+
with Enforcement():
|
|
128
|
+
with pytest.raises(PreconditionFailed):
|
|
129
|
+
BaseFooable.class_foo(50)
|
|
130
|
+
with Enforcement():
|
|
131
|
+
DerivedFooable.class_foo(50)
|
|
132
|
+
|
|
133
|
+
def test_enforcement_of_static_methods(self) -> None:
|
|
134
|
+
with Enforcement():
|
|
135
|
+
DerivedFooable.static_foo(50)
|
|
136
|
+
with pytest.raises(PreconditionFailed):
|
|
137
|
+
BaseFooable.static_foo(50)
|
|
138
|
+
|
|
139
|
+
def test_super_method_enforced(self) -> None:
|
|
140
|
+
with Enforcement():
|
|
141
|
+
with pytest.raises(PreconditionFailed):
|
|
142
|
+
DerivedFooable().foo_only_in_super(50)
|
|
143
|
+
with pytest.raises(PreconditionFailed):
|
|
144
|
+
DerivedFooable().foo(-1)
|
|
145
|
+
# Derived class has a weaker precondition, so this is OK:
|
|
146
|
+
DerivedFooable().foo(50)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class WithMetaclass(metaclass=abc.ABCMeta):
|
|
150
|
+
"""inv: x != 55"""
|
|
151
|
+
|
|
152
|
+
def __init__(self, x):
|
|
153
|
+
"""pre: x != 22"""
|
|
154
|
+
self.x = x
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_skip_init_when_new_returns_different_type():
|
|
158
|
+
COUNTER = [0]
|
|
159
|
+
|
|
160
|
+
class ClassWithInit:
|
|
161
|
+
def __init__(self):
|
|
162
|
+
COUNTER[0] += 1
|
|
163
|
+
|
|
164
|
+
objwithinit = ClassWithInit()
|
|
165
|
+
assert COUNTER[0] == 1
|
|
166
|
+
|
|
167
|
+
class ClassWithNew:
|
|
168
|
+
def __new__(self):
|
|
169
|
+
return objwithinit
|
|
170
|
+
|
|
171
|
+
assert manual_constructor(ClassWithNew)() is objwithinit
|
|
172
|
+
|
|
173
|
+
assert COUNTER[0] == 1 # ensure we did not call __init__ again
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_enforcement_init_on_abcmeta() -> None:
|
|
177
|
+
with Enforcement():
|
|
178
|
+
with pytest.raises(PreconditionFailed):
|
|
179
|
+
WithMetaclass(22)
|
|
180
|
+
with pytest.raises(PostconditionFailed):
|
|
181
|
+
WithMetaclass(55)
|
|
182
|
+
WithMetaclass(99)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# crosshair: analysis_kind=PEP316
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class Farm:
|
|
2
|
+
def visit_chickens(self) -> str:
|
|
3
|
+
return "cluck"
|
|
4
|
+
|
|
5
|
+
def visit_cows(self) -> str:
|
|
6
|
+
return "moo"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def visit_animals(animal: str) -> str:
|
|
10
|
+
"""
|
|
11
|
+
post: __return__ != "moo"
|
|
12
|
+
"""
|
|
13
|
+
try:
|
|
14
|
+
return getattr(Farm(), "visit_" + animal)()
|
|
15
|
+
except BaseException:
|
|
16
|
+
return ""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class HasConsistentHash:
|
|
5
|
+
"""
|
|
6
|
+
A mixin to enforce that classes have hash methods that are consistent
|
|
7
|
+
with their equality checks.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __eq__(self, other: object) -> bool:
|
|
11
|
+
"""
|
|
12
|
+
post: implies(__return__, hash(self) == hash(other))
|
|
13
|
+
"""
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclasses.dataclass
|
|
18
|
+
class Apples(HasConsistentHash):
|
|
19
|
+
"""
|
|
20
|
+
Uses HasConsistentHash to discover that the __eq__ method is
|
|
21
|
+
missing a test for the `count` attribute.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
count: int
|
|
25
|
+
kind: str
|
|
26
|
+
|
|
27
|
+
def __hash__(self):
|
|
28
|
+
return self.count + hash(self.kind)
|
|
29
|
+
|
|
30
|
+
def __eq__(self, other: object) -> bool:
|
|
31
|
+
return isinstance(other, Apples) and self.kind == other.kind
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Dict, List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ShoppingCart:
|
|
5
|
+
"""
|
|
6
|
+
inv: all(quantity > 0 for (_, quantity) in self.items)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, items: Optional[List[Tuple[str, int]]] = None):
|
|
10
|
+
self.items = items or []
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def compute_total(cart: ShoppingCart, prices: Dict[str, float]) -> float:
|
|
14
|
+
"""
|
|
15
|
+
pre: len(cart.items) > 0
|
|
16
|
+
pre: all(pid in prices for (pid, _) in cart.items)
|
|
17
|
+
|
|
18
|
+
We try to ensure that you can't check out with a zero (or less) total.
|
|
19
|
+
However, we forgot a precondition; namely that the `prices` dictionary only has
|
|
20
|
+
prices that are greater than zero.
|
|
21
|
+
|
|
22
|
+
post: __return__ > 0
|
|
23
|
+
"""
|
|
24
|
+
return sum(prices[pid] * quantity for (pid, quantity) in cart.items)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Callable, Dict, List, Sequence, Tuple, TypeVar
|
|
2
|
+
|
|
3
|
+
T = TypeVar("T")
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def list_to_dict(s: Sequence[T]) -> Dict[T, T]:
|
|
7
|
+
"""
|
|
8
|
+
post: len(__return__) == len(s)
|
|
9
|
+
# False; CrossHair finds a counterexample with duplicate values in the input.
|
|
10
|
+
"""
|
|
11
|
+
return dict(zip(s, s))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def consecutive_pairs(x: List[T]) -> List[Tuple[T, T]]:
|
|
15
|
+
"""
|
|
16
|
+
post: len(__return__) == len(x) - 1
|
|
17
|
+
# False (on an empty input list)
|
|
18
|
+
"""
|
|
19
|
+
return [(x[i], x[i + 1]) for i in range(len(x) - 1)]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def higher_order(fn: Callable[[int], int]) -> int:
|
|
23
|
+
"""
|
|
24
|
+
Crosshair can find models for pure callables over atomic types.
|
|
25
|
+
|
|
26
|
+
post: _ != 42
|
|
27
|
+
# False (when given something like lambda a: 42 if (a == 0) else 0)
|
|
28
|
+
"""
|
|
29
|
+
return fn(fn(100))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def append_fourtytwo_to_each(lists: List[List[int]]):
|
|
33
|
+
"""
|
|
34
|
+
pre: len(lists) >= 2
|
|
35
|
+
post: all(len(x) == len(__old__.lists[i]) + 1 for i, x in enumerate(lists))
|
|
36
|
+
# False when two elements of the input are the SAME list!
|
|
37
|
+
"""
|
|
38
|
+
for ls in lists:
|
|
39
|
+
ls.append(42)
|
|
File without changes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def perimiter_length(length: int, width: int) -> int:
|
|
5
|
+
"""
|
|
6
|
+
pre: l > 0 and w > 0
|
|
7
|
+
|
|
8
|
+
The perimeter of a rectangle is longer than any single side:
|
|
9
|
+
post: _ > l and _ > w
|
|
10
|
+
"""
|
|
11
|
+
return 2 * length + 2 * width
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def swap(things: Tuple[int, int]) -> Tuple[int, int]:
|
|
15
|
+
"""
|
|
16
|
+
Swap the arguments.
|
|
17
|
+
|
|
18
|
+
post: _[0] == things[1]
|
|
19
|
+
post: _[1] == things[0]
|
|
20
|
+
"""
|
|
21
|
+
return (things[1], things[0])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# NOTE: To perform additional testing, you can write extra private functions like this one:
|
|
25
|
+
def _assert_double_swap_does_nothing(things: Tuple[int, int]) -> Tuple[int, int]:
|
|
26
|
+
"""
|
|
27
|
+
post: _ == things
|
|
28
|
+
"""
|
|
29
|
+
ret = swap(swap(things))
|
|
30
|
+
return ret
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def double(items: List[str]) -> List[str]:
|
|
34
|
+
"""
|
|
35
|
+
Return a new list that is the input list, repeated twice.
|
|
36
|
+
|
|
37
|
+
post: len(_) == len(items) * 2
|
|
38
|
+
"""
|
|
39
|
+
return items + items
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# NOTE: This is an example of contracts on recursive functions.
|
|
43
|
+
def smallest_two(numbers: Tuple[int, ...]) -> Tuple[Optional[int], Optional[int]]:
|
|
44
|
+
"""
|
|
45
|
+
Find the two smallest numbers.
|
|
46
|
+
|
|
47
|
+
pre: len(numbers) > 0
|
|
48
|
+
# The left return value is always the smallest
|
|
49
|
+
post: _[0] == min(numbers)
|
|
50
|
+
"""
|
|
51
|
+
if len(numbers) == 1:
|
|
52
|
+
return (numbers[0], None)
|
|
53
|
+
(smallest, second) = smallest_two(numbers[1:])
|
|
54
|
+
n = numbers[0]
|
|
55
|
+
if smallest is None or n < smallest:
|
|
56
|
+
return (n, smallest)
|
|
57
|
+
elif second is None or n < second:
|
|
58
|
+
return (smallest, n)
|
|
59
|
+
else:
|
|
60
|
+
return (smallest, second)
|