-
Notifications
You must be signed in to change notification settings - Fork 98
Expand file tree
/
Copy pathtest_util.py
More file actions
385 lines (336 loc) · 11.9 KB
/
Copy pathtest_util.py
File metadata and controls
385 lines (336 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
import pathlib
import sys
from copy import deepcopy
from dataclasses import dataclass, replace
from decimal import Decimal
from math import isnan
from numbers import Real
from typing import (
Callable,
Collection,
Dict,
Iterable,
List,
Mapping,
Optional,
Sequence,
Set,
Tuple,
)
from crosshair.core import (
AnalysisMessage,
Checkable,
MessageType,
analyze_function,
deep_realize,
run_checkables,
)
from crosshair.options import AnalysisOptionSet
from crosshair.statespace import context_statespace
from crosshair.tracers import NoTracing, ResumedTracing
from crosshair.util import (
assert_tracing,
ch_stack,
debug,
in_debug,
is_iterable,
is_pure_python,
name_of_type,
)
ComparableLists = Tuple[List, List]
class _Missing:
pass
_MISSING = _Missing()
def simplefs(path: pathlib.Path, files: dict) -> None:
for name, contents in files.items():
subpath = path / name
if isinstance(contents, str):
with open(subpath, "w") as fh:
fh.write(contents)
elif isinstance(contents, dict):
subpath.mkdir()
simplefs(subpath, contents)
else:
raise Exception("bad input to simplefs")
def check_states(
fn: Callable,
expected: MessageType,
optionset: AnalysisOptionSet = AnalysisOptionSet(),
) -> None:
if expected == MessageType.POST_FAIL:
local_opts = AnalysisOptionSet(
per_condition_timeout=16,
max_uninteresting_iterations=sys.maxsize,
)
elif expected == MessageType.CONFIRMED:
local_opts = AnalysisOptionSet(
per_condition_timeout=60,
per_path_timeout=20,
max_uninteresting_iterations=sys.maxsize,
)
elif expected == MessageType.POST_ERR:
local_opts = AnalysisOptionSet(max_iterations=20)
elif expected == MessageType.CANNOT_CONFIRM:
local_opts = AnalysisOptionSet(
max_uninteresting_iterations=40,
per_condition_timeout=3,
)
else:
local_opts = AnalysisOptionSet(
max_uninteresting_iterations=40,
per_condition_timeout=5,
)
options = local_opts.overlay(optionset)
found = set([m.state for m in run_checkables(analyze_function(fn, options))])
assertmsg = f"Got {','.join(map(str, found))} instead of {expected}"
if not in_debug():
assertmsg += " (use `pytest -v` to show trace)"
assert found == {expected}, assertmsg
def check_exec_err(
fn: Callable, message_prefix="", optionset: AnalysisOptionSet = AnalysisOptionSet()
) -> ComparableLists:
local_opts = AnalysisOptionSet(max_iterations=20)
options = local_opts.overlay(optionset)
messages = run_checkables(analyze_function(fn, options))
if all(m.message.startswith(message_prefix) for m in messages):
return ([m.state for m in messages], [MessageType.EXEC_ERR])
else:
return (
[(m.state, m.message) for m in messages],
[(MessageType.EXEC_ERR, message_prefix)],
)
def check_messages(checkables: Iterable[Checkable], **kw) -> ComparableLists:
msgs = run_checkables(checkables)
if kw.get("state") != MessageType.CONFIRMED:
# Normally, ignore confirmation messages:
msgs = [m for m in msgs if m.state != MessageType.CONFIRMED]
else:
# When we want CONFIRMED, take the message with the worst status:
msgs = [max(msgs, key=lambda m: m.state)]
default_msg = AnalysisMessage(MessageType.CANNOT_CONFIRM, "", "", 0, 0, "")
msg = msgs[0] if msgs else replace(default_msg)
fields = (
"state",
"message",
"filename",
"line",
"column",
"traceback",
"test_fn",
"condition_src",
)
for k in fields:
if k not in kw:
default_val = getattr(default_msg, k)
msg = replace(msg, **{k: default_val})
kw[k] = default_val
if msgs:
msgs[0] = msg
return (msgs, [AnalysisMessage(**kw)])
_NAN_ABLE = (Decimal, Real)
class _Unrealizable:
"""
Sentinel type returned by `safe_deep_realize` when realization fails.
Treated as equal to anything by `flexible_equal`, so unrealizable values
do not poison comparisons (e.g. an arg that became a closed I/O stream).
"""
def __repr__(self) -> str:
return "<unrealizable>"
UNREALIZABLE = _Unrealizable()
def safe_deep_realize(
value: object, label: str = "", memo: Optional[Dict] = None
) -> object:
"""
Best-effort `deep_realize`.
On any exception, debug-logs the failure and returns `UNREALIZABLE`,
which `flexible_equal` treats as equal to anything. This is useful for
diagnostic / comparison contexts (like post-state capture) where we
don't want a non-realizable concrete object to mask the actual result.
Pass a shared `memo` to preserve identity across multiple calls when
realizing several values that may share substructure.
"""
try:
return deep_realize(value, memo)
except Exception as exc:
debug(
"Could not realize",
label or type(value).__name__,
":",
type(exc).__name__,
exc,
)
return UNREALIZABLE
def flexible_equal(a: object, b: object) -> bool:
if a is b:
return True
if a is UNREALIZABLE or b is UNREALIZABLE:
return True
if type(a) is type(b) and type(a).__eq__ is object.__eq__:
# If types match and it uses identity-equals, we can't do much. Assume equal.
return True
if isinstance(a, _NAN_ABLE) and isinstance(b, _NAN_ABLE) and isnan(a) and isnan(b):
return True
if (
is_iterable(a)
and not isinstance(a, Collection)
and is_iterable(b)
and not isinstance(b, Collection)
): # unsized iterables compare by contents
a, b = list(a), list(b) # type: ignore
if (
type(a) == type(b)
and isinstance(a, Collection)
and not isinstance(a, (str, bytes, Set))
):
# Recursively apply flexible_equal for most containers:
if len(a) != len(b): # type: ignore
return False
if isinstance(a, Mapping):
for k, v in a.items():
if not flexible_equal(v, b.get(k, _MISSING)): # type: ignore
return False
return True
else:
return all(flexible_equal(ai, bi) for ai, bi in zip(a, b)) # type: ignore
return a == b
@dataclass(eq=False)
class ExecutionResult:
ret: object # return value
exc: Optional[BaseException] # exception raised, if any
tb: Optional[str]
# args after the function terminates:
post_args: Sequence
post_kwargs: Mapping[str, object]
def __eq__(self, other: object) -> bool:
if not isinstance(other, ExecutionResult):
return False
return (
flexible_equal(self.ret, other.ret)
and type(self.exc) == type(other.exc)
and flexible_equal(self.post_args, other.post_args)
and flexible_equal(self.post_kwargs, other.post_kwargs)
)
def describe(self, include_postexec=False) -> str:
ret = ""
if self.exc:
exc = self.exc
exc_type = name_of_type(type(exc))
tb = self.tb or "(missing traceback)"
ret = f"exc={exc_type}: {str(exc)} {tb}"
else:
ret = f"ret={self.ret!r}"
if include_postexec:
a = [repr(a) for a in self.post_args]
a += [f"{k}={v!r}" for k, v in self.post_kwargs.items()]
ret += f' post=({", ".join(a)})'
return ret
@dataclass
class IterableResult:
values: tuple
typ: type
def summarize_execution(
fn: Callable,
args: Sequence[object] = (),
kwargs: Optional[Mapping[str, object]] = None,
detach_path: bool = True,
) -> ExecutionResult:
if not kwargs:
kwargs = {}
ret: object = None
exc: Optional[Exception] = None
tbstr: Optional[str] = None
try:
possibly_symbolic_ret = fn(*args, **kwargs)
if detach_path:
context_statespace().detach_path()
detach_path = False
ret_type = type(possibly_symbolic_ret)
_ret = deep_realize(possibly_symbolic_ret)
if hasattr(_ret, "__next__"):
# Summarize any iterator as the values it produces, plus its type:
ret = IterableResult(tuple(_ret), ret_type)
elif callable(_ret) and not is_pure_python(_ret):
# Summarize C-based callables just based on their type:
ret = f"C-based callable {type(_ret).__name__}"
else:
ret = _ret
except Exception as e:
exc = e
if detach_path:
context_statespace().detach_path(e)
exc = deep_realize(exc)
# NOTE: deep_realize somehow empties the __traceback__ member; re-assign it:
exc.__traceback__ = e.__traceback__
tbstr = ch_stack(currently_handling=exc)
if in_debug():
debug("hit exception:", type(exc), exc, tbstr)
# Per-element best-effort realization: an unrealizable arg becomes
# UNREALIZABLE (compares equal to anything in flexible_equal) without
# poisoning sibling args. A shared memo preserves identity across
# arguments that alias the same object.
memo: Dict = {}
args = tuple(
safe_deep_realize(a, label=f"argument {idx + 1}", memo=memo)
for idx, a in enumerate(args)
)
kwargs = {
k: safe_deep_realize(v, label=f"keyword argument {k!r}", memo=memo)
for k, v in kwargs.items()
}
return ExecutionResult(ret, exc, tbstr, args, kwargs)
@dataclass
class ResultComparison:
left: ExecutionResult
right: ExecutionResult
def __bool__(self):
return self.left == self.right and type(self.left) == type(self.right)
def __repr__(self):
left, right = self.left, self.right
include_postexec = left.ret == right.ret and type(left.exc) == type(right.exc)
return (
left.describe(include_postexec)
+ " <--symbolic-vs-concrete--> "
+ right.describe(include_postexec)
)
def compare_returns(fn: Callable, *a: object, **kw: object) -> ResultComparison:
comparison = compare_results(fn, *a, **kw)
comparison.left.post_args = ()
comparison.left.post_kwargs = {}
comparison.right.post_args = ()
comparison.right.post_kwargs = {}
return comparison
@assert_tracing(True)
def compare_results(fn: Callable, *a: object, **kw: object) -> ResultComparison:
original_a = deepcopy(a)
original_kw = deepcopy(kw)
symbolic_result = summarize_execution(fn, a, kw)
concrete_a = deep_realize(original_a)
concrete_kw = deep_realize(original_kw)
# Check that realization worked, too:
with NoTracing():
labels_and_args = [
*(
(f"Argument {idx + 1}", a[idx], arg)
for idx, arg in enumerate(concrete_a)
),
*((f"Keyword argument '{k}'", kw[k], v) for k, v in concrete_kw.items()),
]
for label, symbolic_arg, concrete_arg in labels_and_args:
with ResumedTracing():
symbolic_type = type(symbolic_arg)
concrete_type = type(concrete_arg)
true_concrete_type = type(concrete_arg)
assert (
true_concrete_type == concrete_type
), f"{label} did not realize. It is {true_concrete_type} instead of {concrete_type}."
assert (
true_concrete_type == symbolic_type
), f"{label} should realize to {symbolic_type}; it is {true_concrete_type} instead."
with NoTracing():
concrete_result = summarize_execution(
fn, concrete_a, concrete_kw, detach_path=False
)
debug("concrete_result:", concrete_result)
ret = ResultComparison(symbolic_result, concrete_result)
bool(ret)
return ret