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/test_util.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import sys
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from dataclasses import dataclass, replace
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from math import isnan
|
|
7
|
+
from numbers import Real
|
|
8
|
+
from typing import (
|
|
9
|
+
Callable,
|
|
10
|
+
Collection,
|
|
11
|
+
Iterable,
|
|
12
|
+
List,
|
|
13
|
+
Mapping,
|
|
14
|
+
Optional,
|
|
15
|
+
Sequence,
|
|
16
|
+
Set,
|
|
17
|
+
Tuple,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from crosshair.core import (
|
|
21
|
+
AnalysisMessage,
|
|
22
|
+
Checkable,
|
|
23
|
+
MessageType,
|
|
24
|
+
analyze_function,
|
|
25
|
+
deep_realize,
|
|
26
|
+
run_checkables,
|
|
27
|
+
)
|
|
28
|
+
from crosshair.options import AnalysisOptionSet
|
|
29
|
+
from crosshair.statespace import context_statespace
|
|
30
|
+
from crosshair.tracers import NoTracing, ResumedTracing
|
|
31
|
+
from crosshair.util import (
|
|
32
|
+
assert_tracing,
|
|
33
|
+
ch_stack,
|
|
34
|
+
debug,
|
|
35
|
+
in_debug,
|
|
36
|
+
is_iterable,
|
|
37
|
+
is_pure_python,
|
|
38
|
+
name_of_type,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
ComparableLists = Tuple[List, List]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _Missing:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_MISSING = _Missing()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def simplefs(path: pathlib.Path, files: dict) -> None:
|
|
52
|
+
for name, contents in files.items():
|
|
53
|
+
subpath = path / name
|
|
54
|
+
if isinstance(contents, str):
|
|
55
|
+
with open(subpath, "w") as fh:
|
|
56
|
+
fh.write(contents)
|
|
57
|
+
elif isinstance(contents, dict):
|
|
58
|
+
subpath.mkdir()
|
|
59
|
+
simplefs(subpath, contents)
|
|
60
|
+
else:
|
|
61
|
+
raise Exception("bad input to simplefs")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def check_states(
|
|
65
|
+
fn: Callable,
|
|
66
|
+
expected: MessageType,
|
|
67
|
+
optionset: AnalysisOptionSet = AnalysisOptionSet(),
|
|
68
|
+
) -> None:
|
|
69
|
+
if expected == MessageType.POST_FAIL:
|
|
70
|
+
local_opts = AnalysisOptionSet(
|
|
71
|
+
per_condition_timeout=16,
|
|
72
|
+
max_uninteresting_iterations=sys.maxsize,
|
|
73
|
+
)
|
|
74
|
+
elif expected == MessageType.CONFIRMED:
|
|
75
|
+
local_opts = AnalysisOptionSet(
|
|
76
|
+
per_condition_timeout=60,
|
|
77
|
+
per_path_timeout=20,
|
|
78
|
+
max_uninteresting_iterations=sys.maxsize,
|
|
79
|
+
)
|
|
80
|
+
elif expected == MessageType.POST_ERR:
|
|
81
|
+
local_opts = AnalysisOptionSet(max_iterations=20)
|
|
82
|
+
elif expected == MessageType.CANNOT_CONFIRM:
|
|
83
|
+
local_opts = AnalysisOptionSet(
|
|
84
|
+
max_uninteresting_iterations=40,
|
|
85
|
+
per_condition_timeout=3,
|
|
86
|
+
)
|
|
87
|
+
else:
|
|
88
|
+
local_opts = AnalysisOptionSet(
|
|
89
|
+
max_uninteresting_iterations=40,
|
|
90
|
+
per_condition_timeout=5,
|
|
91
|
+
)
|
|
92
|
+
options = local_opts.overlay(optionset)
|
|
93
|
+
found = set([m.state for m in run_checkables(analyze_function(fn, options))])
|
|
94
|
+
assertmsg = f"Got {','.join(map(str, found))} instead of {expected}"
|
|
95
|
+
if not in_debug():
|
|
96
|
+
assertmsg += " (use `pytest -v` to show trace)"
|
|
97
|
+
assert found == {expected}, assertmsg
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def check_exec_err(
|
|
101
|
+
fn: Callable, message_prefix="", optionset: AnalysisOptionSet = AnalysisOptionSet()
|
|
102
|
+
) -> ComparableLists:
|
|
103
|
+
local_opts = AnalysisOptionSet(max_iterations=20)
|
|
104
|
+
options = local_opts.overlay(optionset)
|
|
105
|
+
messages = run_checkables(analyze_function(fn, options))
|
|
106
|
+
if all(m.message.startswith(message_prefix) for m in messages):
|
|
107
|
+
return ([m.state for m in messages], [MessageType.EXEC_ERR])
|
|
108
|
+
else:
|
|
109
|
+
return (
|
|
110
|
+
[(m.state, m.message) for m in messages],
|
|
111
|
+
[(MessageType.EXEC_ERR, message_prefix)],
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def check_messages(checkables: Iterable[Checkable], **kw) -> ComparableLists:
|
|
116
|
+
msgs = run_checkables(checkables)
|
|
117
|
+
if kw.get("state") != MessageType.CONFIRMED:
|
|
118
|
+
# Normally, ignore confirmation messages:
|
|
119
|
+
msgs = [m for m in msgs if m.state != MessageType.CONFIRMED]
|
|
120
|
+
else:
|
|
121
|
+
# When we want CONFIRMED, take the message with the worst status:
|
|
122
|
+
msgs = [max(msgs, key=lambda m: m.state)]
|
|
123
|
+
default_msg = AnalysisMessage(MessageType.CANNOT_CONFIRM, "", "", 0, 0, "")
|
|
124
|
+
msg = msgs[0] if msgs else replace(default_msg)
|
|
125
|
+
fields = (
|
|
126
|
+
"state",
|
|
127
|
+
"message",
|
|
128
|
+
"filename",
|
|
129
|
+
"line",
|
|
130
|
+
"column",
|
|
131
|
+
"traceback",
|
|
132
|
+
"test_fn",
|
|
133
|
+
"condition_src",
|
|
134
|
+
)
|
|
135
|
+
for k in fields:
|
|
136
|
+
if k not in kw:
|
|
137
|
+
default_val = getattr(default_msg, k)
|
|
138
|
+
msg = replace(msg, **{k: default_val})
|
|
139
|
+
kw[k] = default_val
|
|
140
|
+
if msgs:
|
|
141
|
+
msgs[0] = msg
|
|
142
|
+
return (msgs, [AnalysisMessage(**kw)])
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
_NAN_ABLE = (Decimal, Real)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def flexible_equal(a: object, b: object) -> bool:
|
|
149
|
+
if a is b:
|
|
150
|
+
return True
|
|
151
|
+
if type(a) is type(b) and type(a).__eq__ is object.__eq__:
|
|
152
|
+
# If types match and it uses identity-equals, we can't do much. Assume equal.
|
|
153
|
+
return True
|
|
154
|
+
if isinstance(a, _NAN_ABLE) and isinstance(b, _NAN_ABLE) and isnan(a) and isnan(b):
|
|
155
|
+
return True
|
|
156
|
+
if (
|
|
157
|
+
is_iterable(a)
|
|
158
|
+
and not isinstance(a, Collection)
|
|
159
|
+
and is_iterable(b)
|
|
160
|
+
and not isinstance(b, Collection)
|
|
161
|
+
): # unsized iterables compare by contents
|
|
162
|
+
a, b = list(a), list(b) # type: ignore
|
|
163
|
+
if (
|
|
164
|
+
type(a) == type(b)
|
|
165
|
+
and isinstance(a, Collection)
|
|
166
|
+
and not isinstance(a, (str, bytes, Set))
|
|
167
|
+
):
|
|
168
|
+
# Recursively apply flexible_equal for most containers:
|
|
169
|
+
if len(a) != len(b): # type: ignore
|
|
170
|
+
return False
|
|
171
|
+
if isinstance(a, Mapping):
|
|
172
|
+
for k, v in a.items():
|
|
173
|
+
if not flexible_equal(v, b.get(k, _MISSING)): # type: ignore
|
|
174
|
+
return False
|
|
175
|
+
return True
|
|
176
|
+
else:
|
|
177
|
+
return all(flexible_equal(ai, bi) for ai, bi in zip(a, b)) # type: ignore
|
|
178
|
+
|
|
179
|
+
return a == b
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass(eq=False)
|
|
183
|
+
class ExecutionResult:
|
|
184
|
+
ret: object # return value
|
|
185
|
+
exc: Optional[BaseException] # exception raised, if any
|
|
186
|
+
tb: Optional[str]
|
|
187
|
+
# args after the function terminates:
|
|
188
|
+
post_args: Sequence
|
|
189
|
+
post_kwargs: Mapping[str, object]
|
|
190
|
+
|
|
191
|
+
def __eq__(self, other: object) -> bool:
|
|
192
|
+
if not isinstance(other, ExecutionResult):
|
|
193
|
+
return False
|
|
194
|
+
return (
|
|
195
|
+
flexible_equal(self.ret, other.ret)
|
|
196
|
+
and type(self.exc) == type(other.exc)
|
|
197
|
+
and self.post_args == other.post_args
|
|
198
|
+
and self.post_kwargs == other.post_kwargs
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def describe(self, include_postexec=False) -> str:
|
|
202
|
+
ret = ""
|
|
203
|
+
if self.exc:
|
|
204
|
+
exc = self.exc
|
|
205
|
+
exc_type = name_of_type(type(exc))
|
|
206
|
+
tb = self.tb or "(missing traceback)"
|
|
207
|
+
ret = f"exc={exc_type}: {str(exc)} {tb}"
|
|
208
|
+
else:
|
|
209
|
+
ret = f"ret={self.ret!r}"
|
|
210
|
+
if include_postexec:
|
|
211
|
+
a = [repr(a) for a in self.post_args]
|
|
212
|
+
a += [f"{k}={v!r}" for k, v in self.post_kwargs.items()]
|
|
213
|
+
ret += f' post=({", ".join(a)})'
|
|
214
|
+
return ret
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@dataclass
|
|
218
|
+
class IterableResult:
|
|
219
|
+
values: tuple
|
|
220
|
+
typ: type
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def summarize_execution(
|
|
224
|
+
fn: Callable,
|
|
225
|
+
args: Sequence[object] = (),
|
|
226
|
+
kwargs: Optional[Mapping[str, object]] = None,
|
|
227
|
+
detach_path: bool = True,
|
|
228
|
+
) -> ExecutionResult:
|
|
229
|
+
if not kwargs:
|
|
230
|
+
kwargs = {}
|
|
231
|
+
ret: object = None
|
|
232
|
+
exc: Optional[Exception] = None
|
|
233
|
+
tbstr: Optional[str] = None
|
|
234
|
+
try:
|
|
235
|
+
possibly_symbolic_ret = fn(*args, **kwargs)
|
|
236
|
+
if detach_path:
|
|
237
|
+
context_statespace().detach_path()
|
|
238
|
+
detach_path = False
|
|
239
|
+
ret_type = type(possibly_symbolic_ret)
|
|
240
|
+
_ret = deep_realize(possibly_symbolic_ret)
|
|
241
|
+
if hasattr(_ret, "__next__"):
|
|
242
|
+
# Summarize any iterator as the values it produces, plus its type:
|
|
243
|
+
ret = IterableResult(tuple(_ret), ret_type)
|
|
244
|
+
elif callable(_ret) and not is_pure_python(_ret):
|
|
245
|
+
# Summarize C-based callables just based on their type:
|
|
246
|
+
ret = f"C-based callable {type(_ret).__name__}"
|
|
247
|
+
else:
|
|
248
|
+
ret = _ret
|
|
249
|
+
args = deep_realize(args)
|
|
250
|
+
kwargs = deep_realize(kwargs)
|
|
251
|
+
except Exception as e:
|
|
252
|
+
exc = e
|
|
253
|
+
if detach_path:
|
|
254
|
+
context_statespace().detach_path(e)
|
|
255
|
+
exc = deep_realize(exc)
|
|
256
|
+
# NOTE: deep_realize somehow empties the __traceback__ member; re-assign it:
|
|
257
|
+
exc.__traceback__ = e.__traceback__
|
|
258
|
+
tbstr = ch_stack(currently_handling=exc)
|
|
259
|
+
if in_debug():
|
|
260
|
+
debug("hit exception:", type(exc), exc, tbstr)
|
|
261
|
+
return ExecutionResult(ret, exc, tbstr, args, kwargs)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@dataclass
|
|
265
|
+
class ResultComparison:
|
|
266
|
+
left: ExecutionResult
|
|
267
|
+
right: ExecutionResult
|
|
268
|
+
|
|
269
|
+
def __bool__(self):
|
|
270
|
+
return self.left == self.right and type(self.left) == type(self.right)
|
|
271
|
+
|
|
272
|
+
def __repr__(self):
|
|
273
|
+
left, right = self.left, self.right
|
|
274
|
+
include_postexec = left.ret == right.ret and type(left.exc) == type(right.exc)
|
|
275
|
+
return (
|
|
276
|
+
left.describe(include_postexec)
|
|
277
|
+
+ " <--symbolic-vs-concrete--> "
|
|
278
|
+
+ right.describe(include_postexec)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def compare_returns(fn: Callable, *a: object, **kw: object) -> ResultComparison:
|
|
283
|
+
comparison = compare_results(fn, *a, **kw)
|
|
284
|
+
comparison.left.post_args = ()
|
|
285
|
+
comparison.left.post_kwargs = {}
|
|
286
|
+
comparison.right.post_args = ()
|
|
287
|
+
comparison.right.post_kwargs = {}
|
|
288
|
+
return comparison
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@assert_tracing(True)
|
|
292
|
+
def compare_results(fn: Callable, *a: object, **kw: object) -> ResultComparison:
|
|
293
|
+
original_a = deepcopy(a)
|
|
294
|
+
original_kw = deepcopy(kw)
|
|
295
|
+
symbolic_result = summarize_execution(fn, a, kw)
|
|
296
|
+
|
|
297
|
+
concrete_a = deep_realize(original_a)
|
|
298
|
+
concrete_kw = deep_realize(original_kw)
|
|
299
|
+
|
|
300
|
+
# Check that realization worked, too:
|
|
301
|
+
with NoTracing():
|
|
302
|
+
labels_and_args = [
|
|
303
|
+
*(
|
|
304
|
+
(f"Argument {idx + 1}", a[idx], arg)
|
|
305
|
+
for idx, arg in enumerate(concrete_a)
|
|
306
|
+
),
|
|
307
|
+
*((f"Keyword argument '{k}'", kw[k], v) for k, v in concrete_kw.items()),
|
|
308
|
+
]
|
|
309
|
+
for label, symbolic_arg, concrete_arg in labels_and_args:
|
|
310
|
+
with ResumedTracing():
|
|
311
|
+
symbolic_type = type(symbolic_arg)
|
|
312
|
+
concrete_type = type(concrete_arg)
|
|
313
|
+
true_concrete_type = type(concrete_arg)
|
|
314
|
+
assert (
|
|
315
|
+
true_concrete_type == concrete_type
|
|
316
|
+
), f"{label} did not realize. It is {true_concrete_type} instead of {concrete_type}."
|
|
317
|
+
assert (
|
|
318
|
+
true_concrete_type == symbolic_type
|
|
319
|
+
), f"{label} should realize to {symbolic_type}; it is {true_concrete_type} instead."
|
|
320
|
+
|
|
321
|
+
with NoTracing():
|
|
322
|
+
concrete_result = summarize_execution(
|
|
323
|
+
fn, concrete_a, concrete_kw, detach_path=False
|
|
324
|
+
)
|
|
325
|
+
debug("concrete_result:", concrete_result)
|
|
326
|
+
|
|
327
|
+
ret = ResultComparison(symbolic_result, concrete_result)
|
|
328
|
+
bool(ret)
|
|
329
|
+
return ret
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from crosshair.test_util import flexible_equal
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_flexible_equal():
|
|
5
|
+
assert float("nan") != float("nan")
|
|
6
|
+
assert flexible_equal(float("nan"), float("nan"))
|
|
7
|
+
assert flexible_equal((42, float("nan")), (42, float("nan")))
|
|
8
|
+
assert not flexible_equal([float("nan"), 11], [float("nan"), 22])
|
|
9
|
+
|
|
10
|
+
def gen():
|
|
11
|
+
yield 11
|
|
12
|
+
yield 22
|
|
13
|
+
|
|
14
|
+
assert flexible_equal(gen(), iter([11, 22]))
|
|
15
|
+
assert not flexible_equal(gen(), iter([11, 22, 33]))
|
|
16
|
+
assert not flexible_equal(gen(), iter([11]))
|
|
17
|
+
|
|
18
|
+
ordered_set_1 = {10_000, 20_000} | {30_000}
|
|
19
|
+
ordered_set_2 = {30_000, 20_000} | {10_000}
|
|
20
|
+
assert list(ordered_set_1) != list(ordered_set_2) # (different orderings)
|
|
21
|
+
assert flexible_equal(ordered_set_1, ordered_set_2)
|
|
22
|
+
|
|
23
|
+
ordered_dict_1 = {1: 2, 3: 4}
|
|
24
|
+
ordered_dict_2 = {3: 4, 1: 2}
|
|
25
|
+
assert list(ordered_dict_1.items()) != list(ordered_dict_2.items())
|
|
26
|
+
assert flexible_equal(ordered_dict_1, ordered_dict_2)
|
|
File without changes
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""Check that the help snippets in the doc coincide with the actual output."""
|
|
4
|
+
import argparse
|
|
5
|
+
import os
|
|
6
|
+
import pathlib
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from typing import List, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
import icontract
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Block:
|
|
16
|
+
"""Represent a block in the readme that needs to be checked."""
|
|
17
|
+
|
|
18
|
+
@icontract.require(lambda command: command != "")
|
|
19
|
+
@icontract.require(
|
|
20
|
+
lambda start_line_idx, end_line_idx: start_line_idx <= end_line_idx
|
|
21
|
+
)
|
|
22
|
+
def __init__(self, command: str, start_line_idx: int, end_line_idx: int) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Initialize with the given values.
|
|
25
|
+
|
|
26
|
+
:param command: help command
|
|
27
|
+
:param start_line_idx: index of the first relevant line
|
|
28
|
+
:param end_line_idx: index of the first line excluded from the block
|
|
29
|
+
"""
|
|
30
|
+
self.command = command
|
|
31
|
+
self.start_line_idx = start_line_idx
|
|
32
|
+
self.end_line_idx = end_line_idx
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
HELP_STARTS_RE = re.compile(r"^.. Help starts: (?P<command>.*)$")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_rst(lines: List[str]) -> Tuple[List[Block], List[str]]:
|
|
39
|
+
"""
|
|
40
|
+
Parse the code blocks that represent help commands in the RST file.
|
|
41
|
+
|
|
42
|
+
:param lines: lines of the readme file
|
|
43
|
+
:return: (help blocks, errors if any)
|
|
44
|
+
"""
|
|
45
|
+
blocks = [] # type: List[Block]
|
|
46
|
+
errors = [] # type: List[str]
|
|
47
|
+
|
|
48
|
+
i = 0
|
|
49
|
+
while i < len(lines):
|
|
50
|
+
mtch = HELP_STARTS_RE.match(lines[i])
|
|
51
|
+
if mtch:
|
|
52
|
+
command = mtch.group("command")
|
|
53
|
+
help_ends = ".. Help ends: {}".format(command)
|
|
54
|
+
try:
|
|
55
|
+
end_index = lines.index(help_ends, i)
|
|
56
|
+
except ValueError:
|
|
57
|
+
end_index = -1
|
|
58
|
+
|
|
59
|
+
if end_index == -1:
|
|
60
|
+
return [], ["Could not find the end marker {!r}".format(help_ends)]
|
|
61
|
+
|
|
62
|
+
blocks.append(
|
|
63
|
+
Block(command=command, start_line_idx=i + 1, end_line_idx=end_index)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
i = end_index + 1
|
|
67
|
+
|
|
68
|
+
else:
|
|
69
|
+
i += 1
|
|
70
|
+
|
|
71
|
+
return blocks, errors
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def capture_output_lines(command: str) -> List[str]:
|
|
75
|
+
"""Capture the output of a help command."""
|
|
76
|
+
command_parts = command.split(" ")
|
|
77
|
+
if command_parts[0] in ["python", "python3"]:
|
|
78
|
+
# We need to replace "python" with "sys.executable" on Windows as the environment
|
|
79
|
+
# is not properly inherited.
|
|
80
|
+
command_parts[0] = sys.executable
|
|
81
|
+
|
|
82
|
+
proc = subprocess.Popen(
|
|
83
|
+
command_parts,
|
|
84
|
+
stdout=subprocess.PIPE,
|
|
85
|
+
stderr=subprocess.PIPE,
|
|
86
|
+
encoding="utf-8",
|
|
87
|
+
)
|
|
88
|
+
output, err = proc.communicate()
|
|
89
|
+
if err:
|
|
90
|
+
raise RuntimeError(
|
|
91
|
+
f"The command {command!r} failed with exit code {proc.returncode} and "
|
|
92
|
+
f"stderr:\n{err}"
|
|
93
|
+
)
|
|
94
|
+
# Help text changed in 3.10 argparse; always use the newer text.
|
|
95
|
+
output = output.replace("optional arguments", "options")
|
|
96
|
+
|
|
97
|
+
return output.splitlines()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def output_lines_to_code_block(output_lines: List[str]) -> List[str]:
|
|
101
|
+
"""Translate the output of a help command to a RST code block."""
|
|
102
|
+
result = (
|
|
103
|
+
[".. code-block:: text", ""]
|
|
104
|
+
+ [" " + output_line for output_line in output_lines]
|
|
105
|
+
+ [""]
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
result = [line.rstrip() for line in result]
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def diff(got_lines: List[str], expected_lines: List[str]) -> Optional[str]:
|
|
113
|
+
"""
|
|
114
|
+
Report a difference between the ``got`` and ``expected``.
|
|
115
|
+
|
|
116
|
+
Return None if no difference.
|
|
117
|
+
"""
|
|
118
|
+
if got_lines == expected_lines:
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
result = []
|
|
122
|
+
|
|
123
|
+
result.append("Expected:")
|
|
124
|
+
for i, line in enumerate(expected_lines):
|
|
125
|
+
if i >= len(got_lines) or line != got_lines[i]:
|
|
126
|
+
print("DIFF: {:2d}: {!r}".format(i, line))
|
|
127
|
+
else:
|
|
128
|
+
print("OK : {:2d}: {!r}".format(i, line))
|
|
129
|
+
|
|
130
|
+
result.append("Got:")
|
|
131
|
+
for i, line in enumerate(got_lines):
|
|
132
|
+
if i >= len(expected_lines) or line != expected_lines[i]:
|
|
133
|
+
print("DIFF: {:2d}: {!r}".format(i, line))
|
|
134
|
+
else:
|
|
135
|
+
print("OK : {:2d}: {!r}".format(i, line))
|
|
136
|
+
|
|
137
|
+
return "\n".join(result)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def process_file(path: pathlib.Path, overwrite: bool) -> List[str]:
|
|
141
|
+
"""
|
|
142
|
+
Check or overwrite the help blocks in the given file.
|
|
143
|
+
|
|
144
|
+
:param path: to the doc file
|
|
145
|
+
:param overwrite: if set, overwrite the help blocks
|
|
146
|
+
:return: list of errors, if any
|
|
147
|
+
"""
|
|
148
|
+
text = path.read_text(encoding="utf-8")
|
|
149
|
+
lines = text.splitlines()
|
|
150
|
+
|
|
151
|
+
blocks, errors = parse_rst(lines=lines)
|
|
152
|
+
if errors:
|
|
153
|
+
return errors
|
|
154
|
+
|
|
155
|
+
if len(blocks) == 0:
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
if overwrite:
|
|
159
|
+
result = [] # type: List[str]
|
|
160
|
+
|
|
161
|
+
previous_block = None # type: Optional[Block]
|
|
162
|
+
for block in blocks:
|
|
163
|
+
output_lines = capture_output_lines(command=block.command)
|
|
164
|
+
code_block_lines = output_lines_to_code_block(output_lines=output_lines)
|
|
165
|
+
|
|
166
|
+
if previous_block is None:
|
|
167
|
+
result.extend(lines[: block.start_line_idx])
|
|
168
|
+
else:
|
|
169
|
+
result.extend(lines[previous_block.end_line_idx : block.start_line_idx])
|
|
170
|
+
|
|
171
|
+
result.extend(code_block_lines)
|
|
172
|
+
previous_block = block
|
|
173
|
+
assert previous_block is not None
|
|
174
|
+
result.extend(lines[previous_block.end_line_idx :])
|
|
175
|
+
result.append("") # new line at the end of file
|
|
176
|
+
|
|
177
|
+
path.write_text("\n".join(result))
|
|
178
|
+
else:
|
|
179
|
+
for block in blocks:
|
|
180
|
+
output_lines = capture_output_lines(command=block.command)
|
|
181
|
+
code_block_lines = output_lines_to_code_block(output_lines=output_lines)
|
|
182
|
+
|
|
183
|
+
expected_lines = lines[block.start_line_idx : block.end_line_idx]
|
|
184
|
+
expected_lines = [line.rstrip() for line in expected_lines]
|
|
185
|
+
|
|
186
|
+
error = diff(got_lines=code_block_lines, expected_lines=expected_lines)
|
|
187
|
+
if error:
|
|
188
|
+
return [error]
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def main() -> int:
|
|
193
|
+
"""Execute the main routine."""
|
|
194
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
195
|
+
parser.add_argument(
|
|
196
|
+
"--overwrite",
|
|
197
|
+
help="If set, overwrite the relevant part of the doc in-place.",
|
|
198
|
+
action="store_true",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
args = parser.parse_args()
|
|
202
|
+
overwrite = bool(args.overwrite)
|
|
203
|
+
|
|
204
|
+
this_dir = pathlib.Path(os.path.realpath(__file__)).parent.parent.parent
|
|
205
|
+
|
|
206
|
+
pths = [
|
|
207
|
+
this_dir / "doc" / "source" / "contracts.rst",
|
|
208
|
+
this_dir / "doc" / "source" / "cover.rst",
|
|
209
|
+
this_dir / "doc" / "source" / "diff_behavior.rst",
|
|
210
|
+
this_dir / "doc" / "source" / "contributing.rst",
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
success = True
|
|
214
|
+
|
|
215
|
+
for pth in pths:
|
|
216
|
+
errors = process_file(path=pth, overwrite=overwrite)
|
|
217
|
+
if errors:
|
|
218
|
+
print("One or more errors in {}:".format(pth), file=sys.stderr)
|
|
219
|
+
for error in errors:
|
|
220
|
+
print(error, file=sys.stderr)
|
|
221
|
+
success = False
|
|
222
|
+
|
|
223
|
+
# Also check that the TOC in the README matches the Sphinx TOC:
|
|
224
|
+
indexlines = open(
|
|
225
|
+
this_dir / "doc" / "source" / "index.rst", encoding="utf-8"
|
|
226
|
+
).readlines()
|
|
227
|
+
rst_links = [
|
|
228
|
+
f"latest/{line.strip()}.html"
|
|
229
|
+
for line in indexlines
|
|
230
|
+
if re.fullmatch(r"\s*[a-z_]+\s*", line)
|
|
231
|
+
]
|
|
232
|
+
readme_text = open(this_dir / "README.md", encoding="utf-8").read()
|
|
233
|
+
readme_idx = readme_text.index(
|
|
234
|
+
"## [Documentation]"
|
|
235
|
+
) # find the Documentation section
|
|
236
|
+
readme_links = list(re.findall(r"latest/\w+.html", readme_text[readme_idx:]))
|
|
237
|
+
if rst_links != readme_links:
|
|
238
|
+
success = False
|
|
239
|
+
chapters_only_in_rst = set(rst_links) - set(readme_links)
|
|
240
|
+
chapters_only_in_readme = set(readme_links) - set(rst_links)
|
|
241
|
+
if chapters_only_in_rst:
|
|
242
|
+
print(
|
|
243
|
+
f"Error: chapters in index.rst, but missing from README.md: {list(chapters_only_in_rst)}",
|
|
244
|
+
file=sys.stderr,
|
|
245
|
+
)
|
|
246
|
+
elif chapters_only_in_readme:
|
|
247
|
+
print(
|
|
248
|
+
f"Error: chapters in README.md, but missing from index.rst: {list(chapters_only_in_readme)}",
|
|
249
|
+
file=sys.stderr,
|
|
250
|
+
)
|
|
251
|
+
else:
|
|
252
|
+
print(
|
|
253
|
+
f"Error: chapters in README.md and index.rst have different orderings. {rst_links} != {readme_links}",
|
|
254
|
+
file=sys.stderr,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if not success:
|
|
258
|
+
return -1
|
|
259
|
+
|
|
260
|
+
return 0
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
if __name__ == "__main__":
|
|
264
|
+
sys.exit(main())
|