crosshair-tool 0.0.99__cp312-cp312-macosx_10_13_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- _crosshair_tracers.cpython-312-darwin.so +0 -0
- crosshair/__init__.py +42 -0
- crosshair/__main__.py +8 -0
- crosshair/_mark_stacks.h +790 -0
- crosshair/_preliminaries_test.py +18 -0
- crosshair/_tracers.h +94 -0
- crosshair/_tracers_pycompat.h +522 -0
- crosshair/_tracers_test.py +138 -0
- crosshair/abcstring.py +245 -0
- crosshair/auditwall.py +190 -0
- crosshair/auditwall_test.py +77 -0
- crosshair/codeconfig.py +113 -0
- crosshair/codeconfig_test.py +117 -0
- crosshair/condition_parser.py +1237 -0
- crosshair/condition_parser_test.py +497 -0
- crosshair/conftest.py +30 -0
- crosshair/copyext.py +155 -0
- crosshair/copyext_test.py +84 -0
- crosshair/core.py +1763 -0
- crosshair/core_and_libs.py +149 -0
- crosshair/core_regestered_types_test.py +82 -0
- crosshair/core_test.py +1316 -0
- crosshair/diff_behavior.py +314 -0
- crosshair/diff_behavior_test.py +261 -0
- crosshair/dynamic_typing.py +346 -0
- crosshair/dynamic_typing_test.py +210 -0
- crosshair/enforce.py +282 -0
- crosshair/enforce_test.py +182 -0
- crosshair/examples/PEP316/__init__.py +1 -0
- crosshair/examples/PEP316/bugs_detected/__init__.py +0 -0
- crosshair/examples/PEP316/bugs_detected/getattr_magic.py +16 -0
- crosshair/examples/PEP316/bugs_detected/hash_consistent_with_equals.py +31 -0
- crosshair/examples/PEP316/bugs_detected/shopping_cart.py +24 -0
- crosshair/examples/PEP316/bugs_detected/showcase.py +39 -0
- crosshair/examples/PEP316/correct_code/__init__.py +0 -0
- crosshair/examples/PEP316/correct_code/arith.py +60 -0
- crosshair/examples/PEP316/correct_code/chess.py +77 -0
- crosshair/examples/PEP316/correct_code/nesting_inference.py +17 -0
- crosshair/examples/PEP316/correct_code/numpy_examples.py +132 -0
- crosshair/examples/PEP316/correct_code/rolling_average.py +35 -0
- crosshair/examples/PEP316/correct_code/showcase.py +104 -0
- crosshair/examples/__init__.py +0 -0
- crosshair/examples/check_examples_test.py +146 -0
- crosshair/examples/deal/__init__.py +1 -0
- crosshair/examples/icontract/__init__.py +1 -0
- crosshair/examples/icontract/bugs_detected/__init__.py +0 -0
- crosshair/examples/icontract/bugs_detected/showcase.py +41 -0
- crosshair/examples/icontract/bugs_detected/wrong_sign.py +8 -0
- crosshair/examples/icontract/correct_code/__init__.py +0 -0
- crosshair/examples/icontract/correct_code/arith.py +51 -0
- crosshair/examples/icontract/correct_code/showcase.py +94 -0
- crosshair/fnutil.py +391 -0
- crosshair/fnutil_test.py +75 -0
- crosshair/fuzz_core_test.py +516 -0
- crosshair/libimpl/__init__.py +0 -0
- crosshair/libimpl/arraylib.py +161 -0
- crosshair/libimpl/binascii_ch_test.py +30 -0
- crosshair/libimpl/binascii_test.py +67 -0
- crosshair/libimpl/binasciilib.py +150 -0
- crosshair/libimpl/bisectlib_test.py +23 -0
- crosshair/libimpl/builtinslib.py +5228 -0
- crosshair/libimpl/builtinslib_ch_test.py +1191 -0
- crosshair/libimpl/builtinslib_test.py +3735 -0
- crosshair/libimpl/codecslib.py +86 -0
- crosshair/libimpl/codecslib_test.py +86 -0
- crosshair/libimpl/collectionslib.py +264 -0
- crosshair/libimpl/collectionslib_ch_test.py +252 -0
- crosshair/libimpl/collectionslib_test.py +332 -0
- crosshair/libimpl/copylib.py +23 -0
- crosshair/libimpl/copylib_test.py +18 -0
- crosshair/libimpl/datetimelib.py +2559 -0
- crosshair/libimpl/datetimelib_ch_test.py +354 -0
- crosshair/libimpl/datetimelib_test.py +112 -0
- crosshair/libimpl/decimallib.py +5257 -0
- crosshair/libimpl/decimallib_ch_test.py +78 -0
- crosshair/libimpl/decimallib_test.py +76 -0
- crosshair/libimpl/encodings/__init__.py +23 -0
- crosshair/libimpl/encodings/_encutil.py +187 -0
- crosshair/libimpl/encodings/ascii.py +44 -0
- crosshair/libimpl/encodings/latin_1.py +40 -0
- crosshair/libimpl/encodings/utf_8.py +93 -0
- crosshair/libimpl/encodings_ch_test.py +83 -0
- crosshair/libimpl/fractionlib.py +16 -0
- crosshair/libimpl/fractionlib_test.py +80 -0
- crosshair/libimpl/functoolslib.py +34 -0
- crosshair/libimpl/functoolslib_test.py +56 -0
- crosshair/libimpl/hashliblib.py +30 -0
- crosshair/libimpl/hashliblib_test.py +18 -0
- crosshair/libimpl/heapqlib.py +47 -0
- crosshair/libimpl/heapqlib_test.py +21 -0
- crosshair/libimpl/importliblib.py +18 -0
- crosshair/libimpl/importliblib_test.py +38 -0
- crosshair/libimpl/iolib.py +216 -0
- crosshair/libimpl/iolib_ch_test.py +128 -0
- crosshair/libimpl/iolib_test.py +19 -0
- crosshair/libimpl/ipaddresslib.py +8 -0
- crosshair/libimpl/itertoolslib.py +44 -0
- crosshair/libimpl/itertoolslib_test.py +44 -0
- crosshair/libimpl/jsonlib.py +984 -0
- crosshair/libimpl/jsonlib_ch_test.py +42 -0
- crosshair/libimpl/jsonlib_test.py +51 -0
- crosshair/libimpl/mathlib.py +179 -0
- crosshair/libimpl/mathlib_ch_test.py +44 -0
- crosshair/libimpl/mathlib_test.py +67 -0
- crosshair/libimpl/oslib.py +7 -0
- crosshair/libimpl/pathliblib_test.py +10 -0
- crosshair/libimpl/randomlib.py +178 -0
- crosshair/libimpl/randomlib_test.py +120 -0
- crosshair/libimpl/relib.py +846 -0
- crosshair/libimpl/relib_ch_test.py +169 -0
- crosshair/libimpl/relib_test.py +493 -0
- crosshair/libimpl/timelib.py +72 -0
- crosshair/libimpl/timelib_test.py +82 -0
- crosshair/libimpl/typeslib.py +15 -0
- crosshair/libimpl/typeslib_test.py +36 -0
- crosshair/libimpl/unicodedatalib.py +75 -0
- crosshair/libimpl/unicodedatalib_test.py +42 -0
- crosshair/libimpl/urlliblib.py +23 -0
- crosshair/libimpl/urlliblib_test.py +19 -0
- crosshair/libimpl/weakreflib.py +13 -0
- crosshair/libimpl/weakreflib_test.py +69 -0
- crosshair/libimpl/zliblib.py +15 -0
- crosshair/libimpl/zliblib_test.py +13 -0
- crosshair/lsp_server.py +261 -0
- crosshair/lsp_server_test.py +30 -0
- crosshair/main.py +973 -0
- crosshair/main_test.py +543 -0
- crosshair/objectproxy.py +376 -0
- crosshair/objectproxy_test.py +41 -0
- crosshair/opcode_intercept.py +601 -0
- crosshair/opcode_intercept_test.py +304 -0
- crosshair/options.py +218 -0
- crosshair/options_test.py +10 -0
- crosshair/patch_equivalence_test.py +75 -0
- crosshair/path_cover.py +209 -0
- crosshair/path_cover_test.py +138 -0
- crosshair/path_search.py +161 -0
- crosshair/path_search_test.py +52 -0
- crosshair/pathing_oracle.py +271 -0
- crosshair/pathing_oracle_test.py +21 -0
- crosshair/pure_importer.py +27 -0
- crosshair/pure_importer_test.py +16 -0
- crosshair/py.typed +0 -0
- crosshair/register_contract.py +273 -0
- crosshair/register_contract_test.py +190 -0
- crosshair/simplestructs.py +1165 -0
- crosshair/simplestructs_test.py +283 -0
- crosshair/smtlib.py +24 -0
- crosshair/smtlib_test.py +14 -0
- crosshair/statespace.py +1199 -0
- crosshair/statespace_test.py +108 -0
- crosshair/stubs_parser.py +352 -0
- crosshair/stubs_parser_test.py +43 -0
- crosshair/test_util.py +329 -0
- crosshair/test_util_test.py +26 -0
- crosshair/tools/__init__.py +0 -0
- crosshair/tools/check_help_in_doc.py +264 -0
- crosshair/tools/check_init_and_setup_coincide.py +119 -0
- crosshair/tools/generate_demo_table.py +127 -0
- crosshair/tracers.py +544 -0
- crosshair/tracers_test.py +154 -0
- crosshair/type_repo.py +151 -0
- crosshair/unicode_categories.py +589 -0
- crosshair/unicode_categories_test.py +27 -0
- crosshair/util.py +741 -0
- crosshair/util_test.py +173 -0
- crosshair/watcher.py +307 -0
- crosshair/watcher_test.py +107 -0
- crosshair/z3util.py +76 -0
- crosshair/z3util_test.py +11 -0
- crosshair_tool-0.0.99.dist-info/METADATA +144 -0
- crosshair_tool-0.0.99.dist-info/RECORD +176 -0
- crosshair_tool-0.0.99.dist-info/WHEEL +6 -0
- crosshair_tool-0.0.99.dist-info/entry_points.txt +3 -0
- crosshair_tool-0.0.99.dist-info/licenses/LICENSE +93 -0
- crosshair_tool-0.0.99.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import dataclasses
|
|
3
|
+
import dis
|
|
4
|
+
import enum
|
|
5
|
+
import inspect
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
|
|
9
|
+
|
|
10
|
+
from crosshair import IgnoreAttempt
|
|
11
|
+
from crosshair.condition_parser import condition_parser
|
|
12
|
+
from crosshair.core import ExceptionFilter, Patched, deep_realize, gen_args
|
|
13
|
+
from crosshair.fnutil import FunctionInfo
|
|
14
|
+
from crosshair.options import AnalysisOptions
|
|
15
|
+
from crosshair.statespace import (
|
|
16
|
+
CallAnalysis,
|
|
17
|
+
RootNode,
|
|
18
|
+
StateSpace,
|
|
19
|
+
StateSpaceContext,
|
|
20
|
+
VerificationStatus,
|
|
21
|
+
)
|
|
22
|
+
from crosshair.test_util import flexible_equal
|
|
23
|
+
from crosshair.tracers import (
|
|
24
|
+
COMPOSITE_TRACER,
|
|
25
|
+
CoverageResult,
|
|
26
|
+
CoverageTracingModule,
|
|
27
|
+
NoTracing,
|
|
28
|
+
PushedModule,
|
|
29
|
+
ResumedTracing,
|
|
30
|
+
)
|
|
31
|
+
from crosshair.util import CrosshairUnsupported, IgnoreAttempt, UnexploredPath, debug
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ExceptionEquivalenceType(enum.Enum):
|
|
35
|
+
ALL = "ALL"
|
|
36
|
+
SAME_TYPE = "SAME_TYPE"
|
|
37
|
+
TYPE_AND_MESSAGE = "TYPE_AND_MESSAGE"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclasses.dataclass
|
|
41
|
+
class Result:
|
|
42
|
+
return_repr: str
|
|
43
|
+
error: Optional[str]
|
|
44
|
+
post_execution_args: Dict[str, str]
|
|
45
|
+
|
|
46
|
+
def get_differing_arg_mutations(self, other: "Result") -> Set[str]:
|
|
47
|
+
args1 = self.post_execution_args
|
|
48
|
+
args2 = other.post_execution_args
|
|
49
|
+
differing_args: Set[str] = set()
|
|
50
|
+
for arg in set(args1.keys()) | args2.keys():
|
|
51
|
+
if arg in args1 and arg in args2 and args1[arg] != args2[arg]:
|
|
52
|
+
differing_args.add(arg)
|
|
53
|
+
return differing_args
|
|
54
|
+
|
|
55
|
+
def describe(self, args_to_show: Set[str]) -> str:
|
|
56
|
+
ret = []
|
|
57
|
+
if self.error is not None:
|
|
58
|
+
ret.append(f"raises {self.error}")
|
|
59
|
+
if self.return_repr != "None":
|
|
60
|
+
ret.append(f"returns {self.return_repr}")
|
|
61
|
+
if args_to_show:
|
|
62
|
+
if ret:
|
|
63
|
+
ret.append(", ")
|
|
64
|
+
ret.append("after execution ")
|
|
65
|
+
ret.append(
|
|
66
|
+
", ".join(
|
|
67
|
+
f"{arg}={self.post_execution_args[arg]}" for arg in args_to_show
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
# last resort, be explicit about returning none:
|
|
71
|
+
if not ret:
|
|
72
|
+
ret.append("returns None")
|
|
73
|
+
return "".join(ret)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def describe_behavior(
|
|
77
|
+
fn: Callable, args: inspect.BoundArguments
|
|
78
|
+
) -> Tuple[Any, Optional[BaseException]]:
|
|
79
|
+
with ExceptionFilter() as efilter:
|
|
80
|
+
ret = fn(*args.args, **args.kwargs)
|
|
81
|
+
return (ret, None)
|
|
82
|
+
if efilter.user_exc is not None:
|
|
83
|
+
exc = efilter.user_exc[0]
|
|
84
|
+
debug("user-level exception found", repr(exc), *efilter.user_exc[1])
|
|
85
|
+
return (None, exc)
|
|
86
|
+
if efilter.ignore:
|
|
87
|
+
return (None, IgnoreAttempt())
|
|
88
|
+
assert False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclasses.dataclass
|
|
92
|
+
class BehaviorDiff:
|
|
93
|
+
args: Dict[str, str]
|
|
94
|
+
result1: Result
|
|
95
|
+
result2: Result
|
|
96
|
+
coverage1: CoverageResult
|
|
97
|
+
coverage2: CoverageResult
|
|
98
|
+
|
|
99
|
+
def reverse(self) -> "BehaviorDiff":
|
|
100
|
+
return BehaviorDiff(
|
|
101
|
+
self.args, self.result2, self.result1, self.coverage2, self.coverage1
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def diff_scorer(
|
|
106
|
+
check_opcodes1: Set[int], check_opcodes2: Set[int]
|
|
107
|
+
) -> Callable[[BehaviorDiff], Tuple[float, float]]:
|
|
108
|
+
"""
|
|
109
|
+
Create a function to score the usefulness of behavior diffs.
|
|
110
|
+
|
|
111
|
+
We aim for a minimal number of examples that gives as much coverage of the
|
|
112
|
+
differing opcodes as possible.
|
|
113
|
+
We break ties on smaller examples. (repr-string-length-wise)
|
|
114
|
+
"""
|
|
115
|
+
pass # for pydocstyle
|
|
116
|
+
|
|
117
|
+
def scorer(diff: BehaviorDiff) -> Tuple[float, float]:
|
|
118
|
+
coverage1 = diff.coverage1
|
|
119
|
+
coverage2 = diff.coverage2
|
|
120
|
+
total_opcodes = len(coverage1.all_offsets) + len(coverage2.all_offsets)
|
|
121
|
+
cover1 = len(coverage1.offsets_covered & check_opcodes1)
|
|
122
|
+
cover2 = len(coverage2.offsets_covered & check_opcodes2)
|
|
123
|
+
cover_score = (cover1 + cover2) / total_opcodes
|
|
124
|
+
strlen_score = len(str(diff))
|
|
125
|
+
return (cover_score, strlen_score)
|
|
126
|
+
|
|
127
|
+
return scorer
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def diff_behavior(
|
|
131
|
+
ctxfn1: FunctionInfo,
|
|
132
|
+
ctxfn2: FunctionInfo,
|
|
133
|
+
options: AnalysisOptions,
|
|
134
|
+
exception_equivalence: ExceptionEquivalenceType = ExceptionEquivalenceType.TYPE_AND_MESSAGE,
|
|
135
|
+
) -> Union[str, List[BehaviorDiff]]:
|
|
136
|
+
fn1, sig1 = ctxfn1.callable()
|
|
137
|
+
fn2, sig2 = ctxfn2.callable()
|
|
138
|
+
debug("Resolved signature:", sig1)
|
|
139
|
+
all_diffs: List[BehaviorDiff] = []
|
|
140
|
+
half1, half2 = options.split_limits(0.5)
|
|
141
|
+
with condition_parser(
|
|
142
|
+
options.analysis_kind
|
|
143
|
+
), Patched(), COMPOSITE_TRACER, NoTracing():
|
|
144
|
+
# We attempt both orderings of functions. This helps by:
|
|
145
|
+
# (1) avoiding code path explosions in one of the functions
|
|
146
|
+
# (2) using both signatures (in case they differ)
|
|
147
|
+
all_diffs.extend(
|
|
148
|
+
diff_behavior_with_signature(fn1, fn2, sig1, half1, exception_equivalence)
|
|
149
|
+
)
|
|
150
|
+
all_diffs.extend(
|
|
151
|
+
diff.reverse()
|
|
152
|
+
for diff in diff_behavior_with_signature(
|
|
153
|
+
fn2, fn1, sig2, half2, exception_equivalence
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
debug("diff candidates:", all_diffs)
|
|
157
|
+
# greedily pick results:
|
|
158
|
+
result_diffs = []
|
|
159
|
+
opcodeset1 = set(i.offset for i in dis.get_instructions(fn1.__code__))
|
|
160
|
+
opcodeset2 = set(i.offset for i in dis.get_instructions(fn2.__code__))
|
|
161
|
+
while all_diffs:
|
|
162
|
+
scorer = diff_scorer(opcodeset1, opcodeset2)
|
|
163
|
+
selection = max(all_diffs, key=scorer)
|
|
164
|
+
(num_opcodes, _) = scorer(selection)
|
|
165
|
+
debug("Considering input", selection.args, " num opcodes=", num_opcodes)
|
|
166
|
+
if num_opcodes == 0:
|
|
167
|
+
break
|
|
168
|
+
all_diffs.remove(selection)
|
|
169
|
+
result_diffs.append(selection)
|
|
170
|
+
coverage1, coverage2 = selection.coverage1, selection.coverage2
|
|
171
|
+
if coverage1 is not None and coverage2 is not None:
|
|
172
|
+
opcodeset1 -= coverage1.offsets_covered
|
|
173
|
+
opcodeset2 -= coverage2.offsets_covered
|
|
174
|
+
return result_diffs
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def diff_behavior_with_signature(
|
|
178
|
+
fn1: Callable,
|
|
179
|
+
fn2: Callable,
|
|
180
|
+
sig: inspect.Signature,
|
|
181
|
+
options: AnalysisOptions,
|
|
182
|
+
exception_equivalence: ExceptionEquivalenceType,
|
|
183
|
+
) -> Iterable[BehaviorDiff]:
|
|
184
|
+
search_root = RootNode()
|
|
185
|
+
condition_start = time.monotonic()
|
|
186
|
+
max_uninteresting_iterations = options.get_max_uninteresting_iterations()
|
|
187
|
+
for i in range(1, options.max_iterations):
|
|
188
|
+
debug("Iteration ", i)
|
|
189
|
+
itr_start = time.monotonic()
|
|
190
|
+
if itr_start > condition_start + options.per_condition_timeout:
|
|
191
|
+
debug(
|
|
192
|
+
"Stopping due to --per_condition_timeout=",
|
|
193
|
+
options.per_condition_timeout,
|
|
194
|
+
)
|
|
195
|
+
return
|
|
196
|
+
options.incr("num_paths")
|
|
197
|
+
per_path_timeout = options.get_per_path_timeout()
|
|
198
|
+
space = StateSpace(
|
|
199
|
+
execution_deadline=itr_start + per_path_timeout,
|
|
200
|
+
model_check_timeout=per_path_timeout / 2,
|
|
201
|
+
search_root=search_root,
|
|
202
|
+
)
|
|
203
|
+
with StateSpaceContext(space):
|
|
204
|
+
output = None
|
|
205
|
+
try:
|
|
206
|
+
with ResumedTracing():
|
|
207
|
+
(verification_status, output) = run_iteration(
|
|
208
|
+
fn1, fn2, sig, space, exception_equivalence
|
|
209
|
+
)
|
|
210
|
+
except IgnoreAttempt:
|
|
211
|
+
verification_status = None
|
|
212
|
+
except UnexploredPath:
|
|
213
|
+
verification_status = VerificationStatus.UNKNOWN
|
|
214
|
+
debug("Verification status:", verification_status)
|
|
215
|
+
top_analysis, exhausted = space.bubble_status(
|
|
216
|
+
CallAnalysis(verification_status)
|
|
217
|
+
)
|
|
218
|
+
if output:
|
|
219
|
+
yield output
|
|
220
|
+
if exhausted:
|
|
221
|
+
debug("Stopping due to code path exhaustion. (yay!)")
|
|
222
|
+
options.incr("exhaustion")
|
|
223
|
+
break
|
|
224
|
+
if max_uninteresting_iterations != sys.maxsize:
|
|
225
|
+
iters_since_discovery = getattr(
|
|
226
|
+
search_root.pathing_oracle, "iters_since_discovery"
|
|
227
|
+
)
|
|
228
|
+
assert isinstance(iters_since_discovery, int)
|
|
229
|
+
debug("iters_since_discovery", iters_since_discovery)
|
|
230
|
+
if iters_since_discovery > max_uninteresting_iterations:
|
|
231
|
+
debug(
|
|
232
|
+
"Stopping due to --max_uninteresting_iterations=",
|
|
233
|
+
max_uninteresting_iterations,
|
|
234
|
+
)
|
|
235
|
+
break
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def check_exception_equivalence(
|
|
239
|
+
exception_equivalence_type: ExceptionEquivalenceType,
|
|
240
|
+
exc1: Optional[BaseException],
|
|
241
|
+
exc2: Optional[BaseException],
|
|
242
|
+
) -> bool:
|
|
243
|
+
if exc1 is not None and exc2 is not None:
|
|
244
|
+
if exception_equivalence_type == ExceptionEquivalenceType.ALL:
|
|
245
|
+
return True
|
|
246
|
+
elif exception_equivalence_type == ExceptionEquivalenceType.SAME_TYPE:
|
|
247
|
+
return type(exc1) == type(exc2)
|
|
248
|
+
elif exception_equivalence_type == ExceptionEquivalenceType.TYPE_AND_MESSAGE:
|
|
249
|
+
return repr(exc1) == repr(exc2)
|
|
250
|
+
else:
|
|
251
|
+
raise CrosshairUnsupported("Invalid exception_equivalence type")
|
|
252
|
+
else:
|
|
253
|
+
return (exc1 is None) and (exc2 is None)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def run_iteration(
|
|
257
|
+
fn1: Callable,
|
|
258
|
+
fn2: Callable,
|
|
259
|
+
sig: inspect.Signature,
|
|
260
|
+
space: StateSpace,
|
|
261
|
+
exception_equivalence: ExceptionEquivalenceType,
|
|
262
|
+
) -> Tuple[Optional[VerificationStatus], Optional[BehaviorDiff]]:
|
|
263
|
+
with NoTracing():
|
|
264
|
+
original_args = gen_args(sig)
|
|
265
|
+
args1 = copy.deepcopy(original_args)
|
|
266
|
+
args2 = copy.deepcopy(original_args)
|
|
267
|
+
|
|
268
|
+
with NoTracing():
|
|
269
|
+
coverage_manager = CoverageTracingModule(fn1, fn2)
|
|
270
|
+
with ExceptionFilter() as efilter, PushedModule(coverage_manager):
|
|
271
|
+
return1, exc1 = describe_behavior(fn1, args1)
|
|
272
|
+
return2, exc2 = describe_behavior(fn2, args2)
|
|
273
|
+
if (
|
|
274
|
+
flexible_equal(return1, return2)
|
|
275
|
+
and flexible_equal(args1.arguments, args2.arguments)
|
|
276
|
+
and check_exception_equivalence(exception_equivalence, exc1, exc2)
|
|
277
|
+
):
|
|
278
|
+
# Functions are equivalent if both have the same result,
|
|
279
|
+
# and deemed to have the same kind of error.
|
|
280
|
+
space.detach_path()
|
|
281
|
+
debug("Functions equivalent")
|
|
282
|
+
return (VerificationStatus.CONFIRMED, None)
|
|
283
|
+
space.detach_path()
|
|
284
|
+
debug("Functions differ")
|
|
285
|
+
realized_args = {
|
|
286
|
+
k: repr(deep_realize(v)) for (k, v) in original_args.arguments.items()
|
|
287
|
+
}
|
|
288
|
+
post_execution_args1 = {
|
|
289
|
+
k: repr(deep_realize(v)) for k, v in args1.arguments.items()
|
|
290
|
+
}
|
|
291
|
+
post_execution_args2 = {
|
|
292
|
+
k: repr(deep_realize(v)) for k, v in args2.arguments.items()
|
|
293
|
+
}
|
|
294
|
+
diff = BehaviorDiff(
|
|
295
|
+
realized_args,
|
|
296
|
+
Result(
|
|
297
|
+
repr(deep_realize(return1)),
|
|
298
|
+
repr(deep_realize(exc1)) if exc1 is not None else None,
|
|
299
|
+
post_execution_args1,
|
|
300
|
+
),
|
|
301
|
+
Result(
|
|
302
|
+
repr(deep_realize(return2)),
|
|
303
|
+
repr(deep_realize(exc2)) if exc2 is not None else None,
|
|
304
|
+
post_execution_args2,
|
|
305
|
+
),
|
|
306
|
+
coverage_manager.get_results(fn1),
|
|
307
|
+
coverage_manager.get_results(fn2),
|
|
308
|
+
)
|
|
309
|
+
return (VerificationStatus.REFUTED, diff)
|
|
310
|
+
if efilter.user_exc:
|
|
311
|
+
debug(
|
|
312
|
+
"User-level exception found", repr(efilter.user_exc[0]), efilter.user_exc[1]
|
|
313
|
+
)
|
|
314
|
+
return (None, None)
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
from typing import Callable, List, Optional
|
|
2
|
+
|
|
3
|
+
from crosshair.diff_behavior import (
|
|
4
|
+
BehaviorDiff,
|
|
5
|
+
ExceptionEquivalenceType,
|
|
6
|
+
diff_behavior,
|
|
7
|
+
)
|
|
8
|
+
from crosshair.fnutil import FunctionInfo, walk_qualname
|
|
9
|
+
from crosshair.options import DEFAULT_OPTIONS
|
|
10
|
+
from crosshair.util import debug, set_debug
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _foo1(x: int) -> int:
|
|
14
|
+
if x >= 100:
|
|
15
|
+
return 100
|
|
16
|
+
return x
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
foo1 = FunctionInfo.from_fn(_foo1)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _foo2(x: int) -> int:
|
|
23
|
+
return min(x, 100)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
foo2 = FunctionInfo.from_fn(_foo2)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _foo3(x: int) -> int:
|
|
30
|
+
if x > 1000:
|
|
31
|
+
return 1000
|
|
32
|
+
elif x > 100:
|
|
33
|
+
return 100
|
|
34
|
+
else:
|
|
35
|
+
return x
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
foo3 = FunctionInfo.from_fn(_foo3)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Base:
|
|
42
|
+
def foo(self):
|
|
43
|
+
return 10
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def staticfoo(x: int) -> int:
|
|
47
|
+
return min(x, 100)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Derived(Base):
|
|
51
|
+
def foo(self):
|
|
52
|
+
return 11
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _sum_list_original(int_list):
|
|
56
|
+
count = 0
|
|
57
|
+
for i in int_list:
|
|
58
|
+
count += i
|
|
59
|
+
return count
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _sum_list_rewrite(int_list):
|
|
63
|
+
count = 0
|
|
64
|
+
for i in range(len(int_list)):
|
|
65
|
+
count += int_list[i]
|
|
66
|
+
return count
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _sum_list_rewrite_2(int_list):
|
|
70
|
+
class CustomException(Exception):
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
count = 0
|
|
75
|
+
for i in range(len(int_list)):
|
|
76
|
+
count += int_list[i]
|
|
77
|
+
except: # noqa E722
|
|
78
|
+
raise CustomException()
|
|
79
|
+
return count
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TestBehaviorDiff:
|
|
83
|
+
def test_diff_method(self):
|
|
84
|
+
diffs = diff_behavior(
|
|
85
|
+
walk_qualname(Base, "foo"),
|
|
86
|
+
walk_qualname(Derived, "foo"),
|
|
87
|
+
DEFAULT_OPTIONS.overlay(max_iterations=10),
|
|
88
|
+
)
|
|
89
|
+
assert isinstance(diffs, list)
|
|
90
|
+
assert [(d.result1.return_repr, d.result2.return_repr) for d in diffs] == [
|
|
91
|
+
("10", "11")
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
def test_diff_staticmethod(self):
|
|
95
|
+
diffs = diff_behavior(
|
|
96
|
+
walk_qualname(Base, "staticfoo"),
|
|
97
|
+
foo2,
|
|
98
|
+
DEFAULT_OPTIONS.overlay(max_iterations=10),
|
|
99
|
+
)
|
|
100
|
+
assert diffs == []
|
|
101
|
+
|
|
102
|
+
def test_diff_behavior_same(self) -> None:
|
|
103
|
+
diffs = diff_behavior(foo1, foo2, DEFAULT_OPTIONS.overlay(max_iterations=10))
|
|
104
|
+
assert diffs == []
|
|
105
|
+
|
|
106
|
+
def test_diff_behavior_different(self) -> None:
|
|
107
|
+
diffs = diff_behavior(foo1, foo3, DEFAULT_OPTIONS.overlay(max_iterations=10))
|
|
108
|
+
assert len(diffs) == 1
|
|
109
|
+
diff = diffs[0]
|
|
110
|
+
assert isinstance(diff, BehaviorDiff)
|
|
111
|
+
assert int(diff.args["x"]) > 1000
|
|
112
|
+
assert diff.result1.return_repr == "100"
|
|
113
|
+
assert diff.result2.return_repr == "1000"
|
|
114
|
+
|
|
115
|
+
def test_diff_behavior_mutation(self) -> None:
|
|
116
|
+
def cut_out_item1(a: List[int], i: int):
|
|
117
|
+
a[i : i + 1] = []
|
|
118
|
+
|
|
119
|
+
def cut_out_item2(a: List[int], i: int):
|
|
120
|
+
a[:] = a[:i] + a[i + 1 :]
|
|
121
|
+
|
|
122
|
+
# TODO: this takes longer than I'd like:
|
|
123
|
+
opts = DEFAULT_OPTIONS.overlay(max_iterations=40)
|
|
124
|
+
diffs = diff_behavior(
|
|
125
|
+
FunctionInfo.from_fn(cut_out_item1),
|
|
126
|
+
FunctionInfo.from_fn(cut_out_item2),
|
|
127
|
+
opts,
|
|
128
|
+
)
|
|
129
|
+
assert not isinstance(diffs, str)
|
|
130
|
+
assert len(diffs) == 1
|
|
131
|
+
diff = diffs[0]
|
|
132
|
+
assert len(diff.args["a"]) > 1
|
|
133
|
+
assert diff.args["i"] == "-1"
|
|
134
|
+
|
|
135
|
+
def test_example_coverage(self) -> None:
|
|
136
|
+
# Try to get examples that highlist the differences in the code.
|
|
137
|
+
# Here, we add more conditions for the `return True` path and
|
|
138
|
+
# another case where we used to just `return False`.
|
|
139
|
+
def isack1(s: str) -> bool:
|
|
140
|
+
if s in ("y", "yes"):
|
|
141
|
+
return True
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
def isack2(s: str) -> Optional[bool]:
|
|
145
|
+
if s in ("y", "yes", "Y", "YES"):
|
|
146
|
+
return True
|
|
147
|
+
if s in ("n", "no", "N", "NO"):
|
|
148
|
+
return False
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
diffs = diff_behavior(
|
|
152
|
+
FunctionInfo.from_fn(isack1),
|
|
153
|
+
FunctionInfo.from_fn(isack2),
|
|
154
|
+
DEFAULT_OPTIONS.overlay(max_iterations=20),
|
|
155
|
+
)
|
|
156
|
+
debug("diffs=", diffs)
|
|
157
|
+
assert not isinstance(diffs, str)
|
|
158
|
+
return_vals = set((d.result1.return_repr, d.result2.return_repr) for d in diffs)
|
|
159
|
+
assert return_vals == {("False", "None"), ("False", "True")}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_diff_behavior_lambda() -> None:
|
|
163
|
+
def f(a: Optional[Callable[[int], int]]):
|
|
164
|
+
if a:
|
|
165
|
+
return a(2) + 4
|
|
166
|
+
else:
|
|
167
|
+
return "hello"
|
|
168
|
+
|
|
169
|
+
diffs = diff_behavior(
|
|
170
|
+
FunctionInfo.from_fn(f),
|
|
171
|
+
FunctionInfo.from_fn(f),
|
|
172
|
+
DEFAULT_OPTIONS,
|
|
173
|
+
)
|
|
174
|
+
assert diffs == []
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_diffbehavior_exceptions_default() -> None:
|
|
178
|
+
"""
|
|
179
|
+
Default behavior of `diffbehavior` - treating exceptions as different.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
diffs = diff_behavior(
|
|
183
|
+
FunctionInfo.from_fn(_sum_list_original),
|
|
184
|
+
FunctionInfo.from_fn(_sum_list_rewrite),
|
|
185
|
+
DEFAULT_OPTIONS,
|
|
186
|
+
)
|
|
187
|
+
debug("diffs=", diffs)
|
|
188
|
+
assert len(diffs) == 1 # finds a counter-example
|
|
189
|
+
assert isinstance(diffs[0], BehaviorDiff)
|
|
190
|
+
assert diffs[0].result1
|
|
191
|
+
assert isinstance(diffs[0].result1.error, str)
|
|
192
|
+
assert isinstance(diffs[0].result2.error, str)
|
|
193
|
+
assert diffs[0].result1.error.startswith("TypeError")
|
|
194
|
+
assert diffs[0].result2.error.startswith("TypeError")
|
|
195
|
+
assert (
|
|
196
|
+
diffs[0].result1.error != diffs[0].result2.error
|
|
197
|
+
) # Both code-blocks raise a different type error
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_diffbehavior_exceptions_same_type() -> None:
|
|
201
|
+
"""
|
|
202
|
+
Treat exceptions of the same type as equivalent.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
diffs = diff_behavior(
|
|
206
|
+
FunctionInfo.from_fn(_sum_list_original),
|
|
207
|
+
FunctionInfo.from_fn(_sum_list_rewrite),
|
|
208
|
+
DEFAULT_OPTIONS,
|
|
209
|
+
exception_equivalence=ExceptionEquivalenceType.SAME_TYPE,
|
|
210
|
+
)
|
|
211
|
+
debug("diffs=", diffs)
|
|
212
|
+
assert len(diffs) == 0 # No-counter example, because all TypeErrors are equal
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_diffbehavior_exceptions_all() -> None:
|
|
216
|
+
"""
|
|
217
|
+
Treat exceptions of all types as equivalent.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
diffs = diff_behavior(
|
|
221
|
+
FunctionInfo.from_fn(_sum_list_original),
|
|
222
|
+
FunctionInfo.from_fn(_sum_list_rewrite_2),
|
|
223
|
+
DEFAULT_OPTIONS,
|
|
224
|
+
exception_equivalence=ExceptionEquivalenceType.ALL,
|
|
225
|
+
)
|
|
226
|
+
debug("diffs=", diffs)
|
|
227
|
+
assert len(diffs) == 0 # No-counter example, because all TypeErrors are equal
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_diffbehavior_exceptions_same_type_different() -> None:
|
|
231
|
+
"""
|
|
232
|
+
Find a counter-example when raising different exception types.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
diffs = diff_behavior(
|
|
236
|
+
FunctionInfo.from_fn(_sum_list_original),
|
|
237
|
+
FunctionInfo.from_fn(_sum_list_rewrite_2),
|
|
238
|
+
DEFAULT_OPTIONS,
|
|
239
|
+
exception_equivalence=ExceptionEquivalenceType.SAME_TYPE,
|
|
240
|
+
)
|
|
241
|
+
debug("diffs=", diffs)
|
|
242
|
+
assert (
|
|
243
|
+
len(diffs) == 1
|
|
244
|
+
) # finds a counter-example, because TypeError!=CustomException
|
|
245
|
+
assert isinstance(diffs[0], BehaviorDiff)
|
|
246
|
+
assert isinstance(diffs[0].result1.error, str)
|
|
247
|
+
assert isinstance(diffs[0].result2.error, str)
|
|
248
|
+
assert diffs[0].result1.error.startswith("TypeError")
|
|
249
|
+
assert diffs[0].result2.error.startswith("CustomException")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_diff_behavior_nan() -> None:
|
|
253
|
+
def f(x: float):
|
|
254
|
+
return x
|
|
255
|
+
|
|
256
|
+
diffs = diff_behavior(
|
|
257
|
+
FunctionInfo.from_fn(f),
|
|
258
|
+
FunctionInfo.from_fn(f),
|
|
259
|
+
DEFAULT_OPTIONS,
|
|
260
|
+
)
|
|
261
|
+
assert diffs == []
|