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,271 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from typing import Counter, Dict, List, Optional, Sequence, Tuple
|
|
4
|
+
|
|
5
|
+
from z3 import ExprRef # type: ignore
|
|
6
|
+
|
|
7
|
+
from crosshair.statespace import (
|
|
8
|
+
AbstractPathingOracle,
|
|
9
|
+
DetachedPathNode,
|
|
10
|
+
ModelValueNode,
|
|
11
|
+
NodeLike,
|
|
12
|
+
RootNode,
|
|
13
|
+
SearchTreeNode,
|
|
14
|
+
StateSpace,
|
|
15
|
+
WorstResultNode,
|
|
16
|
+
)
|
|
17
|
+
from crosshair.util import CrossHairInternal, debug, in_debug
|
|
18
|
+
from crosshair.z3util import z3And, z3Not, z3Or
|
|
19
|
+
|
|
20
|
+
CodeLoc = Tuple[str, ...]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CoveragePathingOracle(AbstractPathingOracle):
|
|
24
|
+
"""
|
|
25
|
+
A heuristic that attempts to target under-explored code locations.
|
|
26
|
+
|
|
27
|
+
Code condition counts:
|
|
28
|
+
{code pos: {condition => count}}
|
|
29
|
+
Conditions should be
|
|
30
|
+
a count of conditions
|
|
31
|
+
that lead to the code location
|
|
32
|
+
on some previously explored path
|
|
33
|
+
|
|
34
|
+
When beginning an iteration:
|
|
35
|
+
Pick a code location to target, biasing for those with few visits.
|
|
36
|
+
Bias our decisions based on piror ones that led to the target location.
|
|
37
|
+
|
|
38
|
+
Risks:
|
|
39
|
+
* Coupled decisions aren't understood, e.g. "a xor b" is challenging with underexplored
|
|
40
|
+
leaves in both branches.
|
|
41
|
+
* We may target code locations that are impossible to reach because we've exhausted
|
|
42
|
+
every path that leads to them. (TODO: visit frequency may not be the only appropriate
|
|
43
|
+
metric for selecting target locations)
|
|
44
|
+
* Biases for now-impossible branches "bleed" over into our path.
|
|
45
|
+
This sounds like a problem, but is at least partly intentional - we may hope to reach
|
|
46
|
+
some of the same code locations under different early decisions.
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self):
|
|
51
|
+
self.visits = Counter[CodeLoc]()
|
|
52
|
+
self.iters_since_discovery = 0
|
|
53
|
+
self.summarized_positions: Dict[CodeLoc, Counter[int]] = defaultdict(Counter)
|
|
54
|
+
self.current_path_probabilities: Dict[ExprRef, float] = {}
|
|
55
|
+
self.internalized_expressions: Tuple[Dict[ExprRef, int], Dict[int, int]] = (
|
|
56
|
+
{},
|
|
57
|
+
{},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# TODO: This falls apart for moderately sized with_equal_probabilities
|
|
61
|
+
# because that has many small probability decisions.
|
|
62
|
+
# (even just a 10% change could be much larger than it would be otherwise)
|
|
63
|
+
_delta_probabilities = {-1: 0.1, 0: 0.25, 1: 0.9}
|
|
64
|
+
|
|
65
|
+
def pre_path_hook(self, space: StateSpace) -> None:
|
|
66
|
+
root = space._root
|
|
67
|
+
visits = self.visits
|
|
68
|
+
_delta_probabilities = self._delta_probabilities
|
|
69
|
+
|
|
70
|
+
tweaks: Dict[ExprRef, int] = defaultdict(int)
|
|
71
|
+
rand = root._random
|
|
72
|
+
nondiscovery_iters = self.iters_since_discovery
|
|
73
|
+
summarized_positions = self.summarized_positions
|
|
74
|
+
num_positions = len(summarized_positions)
|
|
75
|
+
recent_discovery = rand.random() > nondiscovery_iters / (nondiscovery_iters + 3)
|
|
76
|
+
if recent_discovery or not num_positions:
|
|
77
|
+
debug("No coverage biasing in effect. (", num_positions, " code locations)")
|
|
78
|
+
self.current_path_probabilities = {}
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
options = list(summarized_positions.items())
|
|
82
|
+
options.sort(key=lambda pair: visits[pair[0]])
|
|
83
|
+
chosen_index = int((root._random.random() ** 2.5) * num_positions)
|
|
84
|
+
(loc, exprs) = options[chosen_index]
|
|
85
|
+
for expr in exprs.keys():
|
|
86
|
+
if expr >= 0:
|
|
87
|
+
tweaks[expr] += 1
|
|
88
|
+
else:
|
|
89
|
+
tweaks[-expr] -= 1
|
|
90
|
+
probabilities = {
|
|
91
|
+
expr: _delta_probabilities[delta] for expr, delta in tweaks.items()
|
|
92
|
+
}
|
|
93
|
+
if in_debug():
|
|
94
|
+
debug("Coverage biasing for code location:", loc)
|
|
95
|
+
debug("(", num_positions, " locations presently known)")
|
|
96
|
+
expr_id_map, _ = self.internalized_expressions
|
|
97
|
+
for expr, exprid in expr_id_map.items():
|
|
98
|
+
probability = probabilities.get(exprid)
|
|
99
|
+
if probability:
|
|
100
|
+
debug("coverage tweaked probability", expr, probability)
|
|
101
|
+
self.current_path_probabilities = probabilities
|
|
102
|
+
|
|
103
|
+
def internalize(self, expr):
|
|
104
|
+
expr_id, id_id = self.internalized_expressions
|
|
105
|
+
myid = id(expr)
|
|
106
|
+
unified_id = id_id.get(myid)
|
|
107
|
+
if unified_id:
|
|
108
|
+
return unified_id
|
|
109
|
+
unified_id = expr_id.get(expr)
|
|
110
|
+
if unified_id:
|
|
111
|
+
id_id[myid] = unified_id
|
|
112
|
+
return unified_id
|
|
113
|
+
expr_id[expr] = myid
|
|
114
|
+
id_id[myid] = myid
|
|
115
|
+
return myid
|
|
116
|
+
|
|
117
|
+
def post_path_hook(self, path: Sequence[SearchTreeNode]) -> None:
|
|
118
|
+
leading_locs = []
|
|
119
|
+
leading_conditions: List[int] = []
|
|
120
|
+
for step, node in enumerate(path[:-1]):
|
|
121
|
+
if not isinstance(node, NodeLike):
|
|
122
|
+
continue
|
|
123
|
+
if isinstance(node, WorstResultNode):
|
|
124
|
+
key = node.stacktail
|
|
125
|
+
if (key not in leading_locs) and (not isinstance(node, ModelValueNode)):
|
|
126
|
+
self.summarized_positions[key] += Counter(leading_conditions)
|
|
127
|
+
leading_locs.append(key)
|
|
128
|
+
next_node = path[step + 1]
|
|
129
|
+
if isinstance(next_node, DetachedPathNode):
|
|
130
|
+
break
|
|
131
|
+
if step + 1 < len(path):
|
|
132
|
+
(is_positive, root_expr) = node.normalized_expr
|
|
133
|
+
expr_signature = (
|
|
134
|
+
self.internalize(root_expr)
|
|
135
|
+
if is_positive
|
|
136
|
+
else -self.internalize(root_expr)
|
|
137
|
+
)
|
|
138
|
+
if next_node == node.positive:
|
|
139
|
+
leading_conditions.append(expr_signature)
|
|
140
|
+
elif next_node == node.negative:
|
|
141
|
+
leading_conditions.append(-expr_signature)
|
|
142
|
+
else:
|
|
143
|
+
raise CrossHairInternal(
|
|
144
|
+
f"{type(path[step])} was followed by {type(path[step+1])}"
|
|
145
|
+
)
|
|
146
|
+
visits = self.visits
|
|
147
|
+
prev_len = len(visits)
|
|
148
|
+
visits += Counter(leading_locs)
|
|
149
|
+
if len(visits) > prev_len:
|
|
150
|
+
self.iters_since_discovery = 0
|
|
151
|
+
else:
|
|
152
|
+
self.iters_since_discovery += 1
|
|
153
|
+
|
|
154
|
+
def decide(
|
|
155
|
+
self,
|
|
156
|
+
root: RootNode,
|
|
157
|
+
node: "WorstResultNode",
|
|
158
|
+
engine_probability: Optional[float],
|
|
159
|
+
) -> float:
|
|
160
|
+
if engine_probability in (0.0, 1.0): # is not None:
|
|
161
|
+
return engine_probability
|
|
162
|
+
default_probability = 0.25
|
|
163
|
+
|
|
164
|
+
path_probabilities = self.current_path_probabilities
|
|
165
|
+
is_positive, n_expr = node.normalized_expr
|
|
166
|
+
n_expr_id = self.internalize(n_expr)
|
|
167
|
+
if n_expr_id not in path_probabilities:
|
|
168
|
+
return (
|
|
169
|
+
default_probability
|
|
170
|
+
if engine_probability is None
|
|
171
|
+
else engine_probability
|
|
172
|
+
)
|
|
173
|
+
true_probability = path_probabilities[n_expr_id]
|
|
174
|
+
return true_probability if is_positive else 1.0 - true_probability
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class BreadthFirstPathingOracle(AbstractPathingOracle):
|
|
178
|
+
def decide(
|
|
179
|
+
self,
|
|
180
|
+
root: RootNode,
|
|
181
|
+
node: "WorstResultNode",
|
|
182
|
+
engine_probability: Optional[float],
|
|
183
|
+
) -> float:
|
|
184
|
+
branch_counter = root._open_coverage[node.stacktail]
|
|
185
|
+
|
|
186
|
+
# If we've never taken a branch at this code location, make sure we try it!
|
|
187
|
+
if bool(branch_counter.pos_ct) != bool(branch_counter.neg_ct):
|
|
188
|
+
if engine_probability != 0.0 and engine_probability != 1.0:
|
|
189
|
+
return 1.0 if branch_counter.neg_ct else 0.0
|
|
190
|
+
if engine_probability is None:
|
|
191
|
+
engine_probability = 0.25
|
|
192
|
+
if engine_probability != 0.0 and engine_probability != 1.0:
|
|
193
|
+
if branch_counter.pos_ct > branch_counter.neg_ct * 2 + 1:
|
|
194
|
+
engine_probability /= 2.0
|
|
195
|
+
elif branch_counter.neg_ct > branch_counter.pos_ct * 2 + 1:
|
|
196
|
+
engine_probability = (1.0 + engine_probability) / 2.0
|
|
197
|
+
return engine_probability
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class PreferNegativeOracle(AbstractPathingOracle):
|
|
201
|
+
def decide(
|
|
202
|
+
self,
|
|
203
|
+
root: RootNode,
|
|
204
|
+
node: "WorstResultNode",
|
|
205
|
+
engine_probability: Optional[float],
|
|
206
|
+
) -> float:
|
|
207
|
+
# When both paths are unexplored, we bias for False.
|
|
208
|
+
# As a heuristic, this tends to prefer early completion:
|
|
209
|
+
# - Loop conditions tend to repeat on True.
|
|
210
|
+
# - Optional[X] turns into Union[X, None] and False conditions
|
|
211
|
+
# biases for the last item in the union.
|
|
212
|
+
# We pick a False value more than 2/3rds of the time to avoid
|
|
213
|
+
# explosions while constructing binary-tree-like objects.
|
|
214
|
+
if engine_probability is not None:
|
|
215
|
+
return engine_probability
|
|
216
|
+
return 0.25
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class ConstrainedOracle(AbstractPathingOracle):
|
|
220
|
+
"""
|
|
221
|
+
A pathing oracle that prefers to take a path that satisfies
|
|
222
|
+
explicitly provided constraints.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
def __init__(self, inner_oracle: AbstractPathingOracle):
|
|
226
|
+
self.inner_oracle = inner_oracle
|
|
227
|
+
self.exprs: List[ExprRef] = []
|
|
228
|
+
|
|
229
|
+
def prefer(self, expr: ExprRef):
|
|
230
|
+
self.exprs.append(expr)
|
|
231
|
+
|
|
232
|
+
def pre_path_hook(self, space: StateSpace) -> None:
|
|
233
|
+
self.space = space
|
|
234
|
+
self.exprs = []
|
|
235
|
+
self.inner_oracle.pre_path_hook(space)
|
|
236
|
+
|
|
237
|
+
def post_path_hook(self, path: Sequence["SearchTreeNode"]) -> None:
|
|
238
|
+
self.inner_oracle.post_path_hook(path)
|
|
239
|
+
|
|
240
|
+
def decide(
|
|
241
|
+
self, root, node: "WorstResultNode", engine_probability: Optional[float]
|
|
242
|
+
) -> float:
|
|
243
|
+
# We always run the inner oracle in case it's tracking something about the path.
|
|
244
|
+
default_probability = self.inner_oracle.decide(root, node, engine_probability)
|
|
245
|
+
if not self.space.is_possible(z3And(*[node.expr, *self.exprs])):
|
|
246
|
+
return 0.0
|
|
247
|
+
elif not self.space.is_possible(z3And(*[z3Not(node.expr), *self.exprs])):
|
|
248
|
+
return 1.0
|
|
249
|
+
else:
|
|
250
|
+
return default_probability
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class RotatingOracle(AbstractPathingOracle):
|
|
254
|
+
def __init__(self, oracles: List[AbstractPathingOracle]):
|
|
255
|
+
self.oracles = oracles
|
|
256
|
+
self.index = -1
|
|
257
|
+
|
|
258
|
+
def pre_path_hook(self, space: StateSpace) -> None:
|
|
259
|
+
oracles = self.oracles
|
|
260
|
+
self.index = (self.index + 1) % len(oracles)
|
|
261
|
+
for oracle in oracles:
|
|
262
|
+
oracle.pre_path_hook(space)
|
|
263
|
+
|
|
264
|
+
def post_path_hook(self, path: Sequence["SearchTreeNode"]) -> None:
|
|
265
|
+
for oracle in self.oracles:
|
|
266
|
+
oracle.post_path_hook(path)
|
|
267
|
+
|
|
268
|
+
def decide(
|
|
269
|
+
self, root, node: "WorstResultNode", engine_probability: Optional[float]
|
|
270
|
+
) -> float:
|
|
271
|
+
return self.oracles[self.index].decide(root, node, engine_probability)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
import z3 # type: ignore
|
|
4
|
+
|
|
5
|
+
from crosshair.pathing_oracle import ConstrainedOracle, PreferNegativeOracle
|
|
6
|
+
from crosshair.statespace import RootNode, SimpleStateSpace, WorstResultNode
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_constrained_oracle():
|
|
10
|
+
oracle = ConstrainedOracle(PreferNegativeOracle())
|
|
11
|
+
x = z3.Int("x")
|
|
12
|
+
root = RootNode()
|
|
13
|
+
space = SimpleStateSpace()
|
|
14
|
+
oracle.pre_path_hook(space)
|
|
15
|
+
oracle.prefer(x >= 7)
|
|
16
|
+
rand = random.Random()
|
|
17
|
+
assert oracle.decide(root, WorstResultNode(rand, x < 7, space.solver), None) == 0.0
|
|
18
|
+
assert oracle.decide(root, WorstResultNode(rand, x >= 3, space.solver), None) == 1.0
|
|
19
|
+
assert (
|
|
20
|
+
oracle.decide(root, WorstResultNode(rand, x == 7, space.solver), None) == 0.25
|
|
21
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from importlib.machinery import FileFinder
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class PreferPureLoaderHook:
|
|
10
|
+
orig_hook: Callable[[str], Any]
|
|
11
|
+
|
|
12
|
+
def __call__(self, path: str):
|
|
13
|
+
finder = self.orig_hook(path)
|
|
14
|
+
if isinstance(finder, FileFinder):
|
|
15
|
+
# Move pure python file loaders to the front
|
|
16
|
+
finder._loaders.sort(key=lambda pair: 0 if pair[0] in (".py", ".pyc") else 1) # type: ignore
|
|
17
|
+
return finder
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@contextmanager
|
|
21
|
+
def prefer_pure_python_imports():
|
|
22
|
+
sys.path_hooks = [PreferPureLoaderHook(h) for h in sys.path_hooks]
|
|
23
|
+
sys.path_importer_cache.clear()
|
|
24
|
+
yield
|
|
25
|
+
assert all(isinstance(h, PreferPureLoaderHook) for h in sys.path_hooks)
|
|
26
|
+
sys.path_hooks = [h.orig_hook for h in sys.path_hooks]
|
|
27
|
+
sys.path_importer_cache.clear()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import sys
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from crosshair.pure_importer import prefer_pure_python_imports
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.skip(
|
|
11
|
+
reason="We used to test pydantic here, but current version doesn't use Cython"
|
|
12
|
+
)
|
|
13
|
+
def test_prefer_pure_python_imports():
|
|
14
|
+
with prefer_pure_python_imports():
|
|
15
|
+
pydantic = importlib.import_module("pydantic")
|
|
16
|
+
assert not pydantic.compiled
|
crosshair/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""API for registering contracts for external libraries."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from inspect import Parameter, Signature, getmodule, ismethod, signature
|
|
5
|
+
from types import MethodDescriptorType, ModuleType, WrapperDescriptorType
|
|
6
|
+
from typing import Callable, Dict, FrozenSet, List, Optional, Set, Union
|
|
7
|
+
from weakref import ReferenceType
|
|
8
|
+
|
|
9
|
+
from crosshair.fnutil import resolve_signature
|
|
10
|
+
from crosshair.stubs_parser import signature_from_stubs
|
|
11
|
+
from crosshair.util import debug, warn
|
|
12
|
+
|
|
13
|
+
# TODO: One might want to add more features to overloading. Currently contracts only
|
|
14
|
+
# support multiple signatures, but not pre- and postconditions depending on the
|
|
15
|
+
# overload. REGISTERED_CONTRACTS might become Dict[Callable, List[ContractOverride]].
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ContractOverride:
|
|
20
|
+
pre: Optional[Callable[..., bool]]
|
|
21
|
+
post: Optional[Callable[..., bool]]
|
|
22
|
+
sigs: List[Signature]
|
|
23
|
+
skip_body: bool
|
|
24
|
+
# TODO: Once supported, we might want to register Exceptions ("raises") as well
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
REGISTERED_CONTRACTS: Dict[Callable, ContractOverride] = {}
|
|
28
|
+
REGISTERED_MODULES: Set[ModuleType] = set()
|
|
29
|
+
|
|
30
|
+
# Don't automatically register those functions.
|
|
31
|
+
_NO_AUTO_REGISTER: FrozenSet[str] = frozenset(
|
|
32
|
+
{
|
|
33
|
+
"__init__",
|
|
34
|
+
"__init_subclass__",
|
|
35
|
+
"__new__",
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def required_param_names(sig: Signature) -> Set[str]:
|
|
41
|
+
return {k for (k, v) in sig.parameters.items() if v.default is Parameter.empty}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _verify_signatures(
|
|
45
|
+
fn: Callable,
|
|
46
|
+
contract: ContractOverride,
|
|
47
|
+
ref_sig: Optional[Signature],
|
|
48
|
+
) -> bool:
|
|
49
|
+
"""Verify the provided signatures (including signatures of `pre` and `post`)."""
|
|
50
|
+
success = True
|
|
51
|
+
if ref_sig:
|
|
52
|
+
all_sigs = contract.sigs.copy()
|
|
53
|
+
all_sigs.append(ref_sig)
|
|
54
|
+
else:
|
|
55
|
+
all_sigs = contract.sigs
|
|
56
|
+
for sig in all_sigs:
|
|
57
|
+
params = set(sig.parameters.keys())
|
|
58
|
+
# First verify the parameters against the reference sig.
|
|
59
|
+
if ref_sig:
|
|
60
|
+
ref_params = set(ref_sig.parameters.keys())
|
|
61
|
+
# Cannot test for strict equality, because of overloads.
|
|
62
|
+
if not required_param_names(sig) <= ref_params:
|
|
63
|
+
warn(
|
|
64
|
+
f"Malformed signature for function {fn.__name__}. "
|
|
65
|
+
f"Expected parameters: {ref_params}, found: {params}",
|
|
66
|
+
)
|
|
67
|
+
success = False
|
|
68
|
+
# Verify the signature of the precondition.
|
|
69
|
+
if contract.pre:
|
|
70
|
+
pre_params = required_param_names(signature(contract.pre))
|
|
71
|
+
if not pre_params <= params:
|
|
72
|
+
warn(
|
|
73
|
+
f"Malformated precondition for function {fn.__name__}. "
|
|
74
|
+
f"Unexpected arguments: {pre_params - params}"
|
|
75
|
+
)
|
|
76
|
+
success = False
|
|
77
|
+
# Verify the signature of the postcondition.
|
|
78
|
+
if contract.post:
|
|
79
|
+
post_params = required_param_names(signature(contract.post))
|
|
80
|
+
params.add("__return__")
|
|
81
|
+
params.add("__old__")
|
|
82
|
+
if not post_params <= params:
|
|
83
|
+
warn(
|
|
84
|
+
f"Malformated postcondition for function {fn.__name__}. "
|
|
85
|
+
f"Unexpected parameters: {post_params - params}."
|
|
86
|
+
)
|
|
87
|
+
success = False
|
|
88
|
+
return success
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _add_contract(
|
|
92
|
+
fn: Callable,
|
|
93
|
+
contract: ContractOverride,
|
|
94
|
+
ref_sig: Optional[Signature],
|
|
95
|
+
) -> bool:
|
|
96
|
+
"""Add a contract to the function and check for consistency."""
|
|
97
|
+
verified = _verify_signatures(fn, contract, ref_sig)
|
|
98
|
+
if not verified:
|
|
99
|
+
return False
|
|
100
|
+
old_contract = REGISTERED_CONTRACTS.get(fn)
|
|
101
|
+
if old_contract:
|
|
102
|
+
if (
|
|
103
|
+
old_contract.pre == contract.pre
|
|
104
|
+
and old_contract.post == contract.post
|
|
105
|
+
and old_contract.skip_body == contract.skip_body
|
|
106
|
+
):
|
|
107
|
+
old_contract.sigs.extend(contract.sigs)
|
|
108
|
+
else:
|
|
109
|
+
warn(
|
|
110
|
+
"Pre- and postconditons and skip_body should not differ when "
|
|
111
|
+
f"registering multiple contracts for the same function: {fn.__name__}."
|
|
112
|
+
)
|
|
113
|
+
return False
|
|
114
|
+
else:
|
|
115
|
+
REGISTERED_CONTRACTS[fn] = contract
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _internal_register_contract(
|
|
120
|
+
fn: Callable,
|
|
121
|
+
pre: Optional[Callable[..., bool]] = None,
|
|
122
|
+
post: Optional[Callable[..., bool]] = None,
|
|
123
|
+
sig: Union[Signature, List[Signature], None] = None,
|
|
124
|
+
skip_body: bool = True,
|
|
125
|
+
) -> bool:
|
|
126
|
+
reference_sig = None
|
|
127
|
+
|
|
128
|
+
sig_or_error = resolve_signature(fn)
|
|
129
|
+
if isinstance(sig_or_error, Signature):
|
|
130
|
+
reference_sig = sig_or_error
|
|
131
|
+
|
|
132
|
+
# In the case the signature is incomplete, look in the stubs.
|
|
133
|
+
if not sig and (
|
|
134
|
+
not reference_sig or reference_sig.return_annotation == Parameter.empty
|
|
135
|
+
):
|
|
136
|
+
sigs, is_valid = signature_from_stubs(fn)
|
|
137
|
+
if sigs:
|
|
138
|
+
debug(f"Found {str(len(sigs))} signature(s) for {fn.__name__} in stubs")
|
|
139
|
+
if not is_valid or any(
|
|
140
|
+
sig.return_annotation == Parameter.empty for sig in sigs
|
|
141
|
+
):
|
|
142
|
+
warn(
|
|
143
|
+
f"Incomplete signature for {fn.__name__} in stubs, consider "
|
|
144
|
+
f"registering the signature manually. Signatures found: "
|
|
145
|
+
f"{str(sigs)}"
|
|
146
|
+
)
|
|
147
|
+
contract = ContractOverride(pre, post, sigs, skip_body)
|
|
148
|
+
return _add_contract(fn, contract, reference_sig)
|
|
149
|
+
else:
|
|
150
|
+
if reference_sig:
|
|
151
|
+
warn(
|
|
152
|
+
f"No valid signature found for function {fn.__name__}, "
|
|
153
|
+
"consider registering the signature manually."
|
|
154
|
+
)
|
|
155
|
+
contract = ContractOverride(pre, post, [], skip_body)
|
|
156
|
+
return _add_contract(fn, contract, reference_sig)
|
|
157
|
+
# No signature available at all, we cannot register the function.
|
|
158
|
+
else:
|
|
159
|
+
warn(
|
|
160
|
+
f"Could not automatically register {fn.__name__}, reason: no "
|
|
161
|
+
"signature found."
|
|
162
|
+
)
|
|
163
|
+
return False
|
|
164
|
+
else:
|
|
165
|
+
# Verify the contract and register it.
|
|
166
|
+
if sig is None:
|
|
167
|
+
sig = []
|
|
168
|
+
elif isinstance(sig, Signature):
|
|
169
|
+
sig = [sig]
|
|
170
|
+
contract = ContractOverride(pre, post, sig, skip_body)
|
|
171
|
+
return _add_contract(fn, contract, reference_sig)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def register_contract(
|
|
175
|
+
fn: Callable,
|
|
176
|
+
*,
|
|
177
|
+
pre: Optional[Callable[..., bool]] = None,
|
|
178
|
+
post: Optional[Callable[..., bool]] = None,
|
|
179
|
+
sig: Union[Signature, List[Signature], None] = None,
|
|
180
|
+
skip_body: bool = True,
|
|
181
|
+
) -> bool:
|
|
182
|
+
"""
|
|
183
|
+
Register a contract for the given function.
|
|
184
|
+
|
|
185
|
+
:param fn: The function to add a contract for.
|
|
186
|
+
:param pre: The preconditon which should hold when entering the function.
|
|
187
|
+
:param post: The postcondition which should hold when returning from the function.
|
|
188
|
+
:param sig: If provided, CrossHair will use this signature for the function.\
|
|
189
|
+
Usefull for manually providing type annotation. You can provide multiple\
|
|
190
|
+
signatures for overloaded functions.
|
|
191
|
+
:param skip_body: By default registered functions will be skipped executing,\
|
|
192
|
+
assuming the postconditions hold. Set this to `False` to still execute the body.
|
|
193
|
+
:returns: A boolean indicating whether the contract was successfully registered.
|
|
194
|
+
"""
|
|
195
|
+
# Don't allow registering bound methods.
|
|
196
|
+
if ismethod(fn):
|
|
197
|
+
cls = getattr(getattr(fn, "__self__", None), "__class__", "<not found>")
|
|
198
|
+
warn(
|
|
199
|
+
f"You registered the bound method {fn}. You should register the unbound "
|
|
200
|
+
f"function of the class {cls} instead."
|
|
201
|
+
)
|
|
202
|
+
return False
|
|
203
|
+
return _internal_register_contract(fn, pre, post, sig, skip_body)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def clear_contract_registrations():
|
|
207
|
+
global REGISTERED_CONTRACTS
|
|
208
|
+
REGISTERED_CONTRACTS.clear()
|
|
209
|
+
global REGISTERED_MODULES
|
|
210
|
+
REGISTERED_MODULES.clear()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def get_contract(fn: Callable) -> Optional[ContractOverride]:
|
|
214
|
+
"""
|
|
215
|
+
Get the contract associated to the given function, it the function was registered.
|
|
216
|
+
|
|
217
|
+
:param fn: The function to retrieve the contract for.
|
|
218
|
+
:return: The contract associated with the function or None if the function was not\
|
|
219
|
+
registered.
|
|
220
|
+
"""
|
|
221
|
+
# Weak references are not hashable: REGISTERED_CONTRACTS.get(fn) fails.
|
|
222
|
+
if isinstance(fn, ReferenceType):
|
|
223
|
+
return None
|
|
224
|
+
# Return the registered contract for the function, if any.
|
|
225
|
+
contract = REGISTERED_CONTRACTS.get(fn)
|
|
226
|
+
if contract:
|
|
227
|
+
return contract
|
|
228
|
+
fn_name = getattr(fn, "__name__", None)
|
|
229
|
+
# Some functions should not be automatically registered.
|
|
230
|
+
if fn_name in _NO_AUTO_REGISTER:
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
# If the function belongs to a registered module, register it.
|
|
234
|
+
module = getmodule(fn)
|
|
235
|
+
# If this is a classmethod, look for the module in the mro.
|
|
236
|
+
fn_self = getattr(fn, "__self__", None)
|
|
237
|
+
if fn_name and fn_self and isinstance(fn_self, type):
|
|
238
|
+
for mro in fn_self.mro():
|
|
239
|
+
if fn_name in mro.__dict__:
|
|
240
|
+
module_name = mro.__module__
|
|
241
|
+
if module_name in map(lambda x: x.__name__, REGISTERED_MODULES):
|
|
242
|
+
_internal_register_contract(fn)
|
|
243
|
+
return REGISTERED_CONTRACTS.get(fn)
|
|
244
|
+
return None
|
|
245
|
+
if module and module in REGISTERED_MODULES:
|
|
246
|
+
_internal_register_contract(fn)
|
|
247
|
+
return REGISTERED_CONTRACTS.get(fn)
|
|
248
|
+
# Some builtins and some C functions are wrapped into Descriptors
|
|
249
|
+
if isinstance(fn, (MethodDescriptorType, WrapperDescriptorType)):
|
|
250
|
+
module_name = fn.__objclass__.__module__
|
|
251
|
+
if module_name in map(lambda x: x.__name__, REGISTERED_MODULES):
|
|
252
|
+
_internal_register_contract(fn)
|
|
253
|
+
return REGISTERED_CONTRACTS.get(fn)
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def register_modules(*modules: ModuleType) -> None:
|
|
258
|
+
"""
|
|
259
|
+
Specify a module whose functions should all be skipped at execution.
|
|
260
|
+
|
|
261
|
+
THIS IS AN EXPERIMENTAL FEATURE!
|
|
262
|
+
Registering a whole module might be too much in some cases and you might fallback to
|
|
263
|
+
register individual functions instead.
|
|
264
|
+
|
|
265
|
+
Note that functions `__init__`, `__init_subclass__` and `__new__` are never
|
|
266
|
+
registered automatically.
|
|
267
|
+
|
|
268
|
+
If you wish to register all functions, except function `foo`, register that function
|
|
269
|
+
manually with the option `skip_body=False`.
|
|
270
|
+
|
|
271
|
+
:param modules: one or multiple modules whose functions should be skipped.
|
|
272
|
+
"""
|
|
273
|
+
REGISTERED_MODULES.update(modules)
|