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/path_cover.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
import enum
|
|
3
|
+
import re
|
|
4
|
+
import traceback
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from inspect import BoundArguments
|
|
7
|
+
from typing import Callable, List, Optional, Set, TextIO, Tuple, Type
|
|
8
|
+
|
|
9
|
+
from crosshair.condition_parser import condition_parser
|
|
10
|
+
from crosshair.core import (
|
|
11
|
+
ExceptionFilter,
|
|
12
|
+
LazyCreationRepr,
|
|
13
|
+
deep_realize,
|
|
14
|
+
explore_paths,
|
|
15
|
+
realize,
|
|
16
|
+
)
|
|
17
|
+
from crosshair.fnutil import FunctionInfo
|
|
18
|
+
from crosshair.options import AnalysisOptions
|
|
19
|
+
from crosshair.statespace import RootNode, StateSpace, context_statespace
|
|
20
|
+
from crosshair.tracers import (
|
|
21
|
+
COMPOSITE_TRACER,
|
|
22
|
+
CoverageResult,
|
|
23
|
+
CoverageTracingModule,
|
|
24
|
+
NoTracing,
|
|
25
|
+
PushedModule,
|
|
26
|
+
)
|
|
27
|
+
from crosshair.util import (
|
|
28
|
+
ReferencedIdentifier,
|
|
29
|
+
ch_stack,
|
|
30
|
+
debug,
|
|
31
|
+
format_boundargs,
|
|
32
|
+
name_of_type,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CoverageType(enum.Enum):
|
|
37
|
+
OPCODE = "OPCODE"
|
|
38
|
+
PATH = "PATH"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class PathSummary:
|
|
43
|
+
args: BoundArguments
|
|
44
|
+
formatted_args: str
|
|
45
|
+
result: str
|
|
46
|
+
exc: Optional[Type[BaseException]]
|
|
47
|
+
exc_message: Optional[str]
|
|
48
|
+
post_args: BoundArguments
|
|
49
|
+
coverage: CoverageResult
|
|
50
|
+
references: Set[ReferencedIdentifier]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def path_cover(
|
|
54
|
+
ctxfn: FunctionInfo,
|
|
55
|
+
options: AnalysisOptions,
|
|
56
|
+
coverage_type: CoverageType,
|
|
57
|
+
arg_formatter: Callable[[BoundArguments], str] = format_boundargs,
|
|
58
|
+
) -> List[PathSummary]:
|
|
59
|
+
fn, sig = ctxfn.callable()
|
|
60
|
+
while getattr(fn, "__wrapped__", None):
|
|
61
|
+
# Usually we don't want to run decorator code. (and we certainly don't want
|
|
62
|
+
# to measure coverage on the decorator rather than the real body) Unwrap:
|
|
63
|
+
fn = fn.__wrapped__ # type: ignore
|
|
64
|
+
search_root = RootNode()
|
|
65
|
+
|
|
66
|
+
paths: List[PathSummary] = []
|
|
67
|
+
coverage: CoverageTracingModule = CoverageTracingModule(fn)
|
|
68
|
+
|
|
69
|
+
def run_path(args: BoundArguments):
|
|
70
|
+
nonlocal coverage
|
|
71
|
+
with NoTracing():
|
|
72
|
+
coverage = CoverageTracingModule(fn)
|
|
73
|
+
with PushedModule(coverage):
|
|
74
|
+
return fn(*args.args, **args.kwargs)
|
|
75
|
+
|
|
76
|
+
def on_path_complete(
|
|
77
|
+
space: StateSpace,
|
|
78
|
+
pre_args: BoundArguments,
|
|
79
|
+
post_args: BoundArguments,
|
|
80
|
+
ret,
|
|
81
|
+
exc: Optional[BaseException],
|
|
82
|
+
exc_stack: Optional[traceback.StackSummary],
|
|
83
|
+
) -> bool:
|
|
84
|
+
with ExceptionFilter() as efilter:
|
|
85
|
+
space.detach_path()
|
|
86
|
+
|
|
87
|
+
reprer = context_statespace().extra(LazyCreationRepr)
|
|
88
|
+
formatted_pre_args = reprer.eval_friendly_format(pre_args, arg_formatter)
|
|
89
|
+
|
|
90
|
+
pre_args = deep_realize(pre_args)
|
|
91
|
+
post_args = deep_realize(post_args)
|
|
92
|
+
ret = reprer.eval_friendly_format(ret, lambda x: builtins.repr(x))
|
|
93
|
+
references = reprer.repr_references
|
|
94
|
+
|
|
95
|
+
cov = coverage.get_results(fn)
|
|
96
|
+
debug("Realized args:", formatted_pre_args)
|
|
97
|
+
if exc is not None:
|
|
98
|
+
debug("user-level exception found", type(exc), exc, ch_stack(exc_stack))
|
|
99
|
+
exc_message = realize(str(exc)) if len(exc.args) > 0 else None
|
|
100
|
+
paths.append(
|
|
101
|
+
PathSummary(
|
|
102
|
+
pre_args,
|
|
103
|
+
formatted_pre_args,
|
|
104
|
+
ret,
|
|
105
|
+
type(exc),
|
|
106
|
+
exc_message,
|
|
107
|
+
post_args,
|
|
108
|
+
cov,
|
|
109
|
+
references,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
debug("Realized return:", ret)
|
|
114
|
+
paths.append(
|
|
115
|
+
PathSummary(
|
|
116
|
+
pre_args,
|
|
117
|
+
formatted_pre_args,
|
|
118
|
+
ret,
|
|
119
|
+
None,
|
|
120
|
+
None,
|
|
121
|
+
post_args,
|
|
122
|
+
cov,
|
|
123
|
+
references,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
return False
|
|
127
|
+
debug("Skipping path (failed to realize values)", efilter.user_exc)
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
explore_paths(run_path, sig, options, search_root, on_path_complete)
|
|
131
|
+
|
|
132
|
+
opcodes_found: Set[int] = set()
|
|
133
|
+
selected: List[PathSummary] = []
|
|
134
|
+
while paths:
|
|
135
|
+
next_best = max(
|
|
136
|
+
paths,
|
|
137
|
+
key=lambda p: (
|
|
138
|
+
len(p.coverage.offsets_covered - opcodes_found), # high coverage
|
|
139
|
+
-len(p.formatted_args), # with small input size
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
cur_offsets = next_best.coverage.offsets_covered
|
|
143
|
+
if coverage_type == CoverageType.OPCODE:
|
|
144
|
+
debug("Next best path covers these opcode offsets:", cur_offsets)
|
|
145
|
+
if len(cur_offsets - opcodes_found) == 0:
|
|
146
|
+
break
|
|
147
|
+
selected.append(next_best)
|
|
148
|
+
opcodes_found |= cur_offsets
|
|
149
|
+
paths = [p for p in paths if p is not next_best]
|
|
150
|
+
return selected
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def output_argument_dictionary_paths(
|
|
154
|
+
fn: Callable, paths: List[PathSummary], stdout: TextIO, stderr: TextIO
|
|
155
|
+
):
|
|
156
|
+
for path in paths:
|
|
157
|
+
stdout.write(path.formatted_args + "\n")
|
|
158
|
+
stdout.flush()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def output_eval_exression_paths(
|
|
162
|
+
fn: Callable, paths: List[PathSummary], stdout: TextIO, stderr: TextIO
|
|
163
|
+
):
|
|
164
|
+
for path in paths:
|
|
165
|
+
stdout.write(fn.__name__ + "(" + path.formatted_args + ")\n")
|
|
166
|
+
stdout.flush()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def import_statements_for_references(references: Set[ReferencedIdentifier]) -> Set[str]:
|
|
170
|
+
imports: Set[str] = set()
|
|
171
|
+
for ref in references:
|
|
172
|
+
if ref.modulename == "builtins":
|
|
173
|
+
continue
|
|
174
|
+
if "." in ref.qualname:
|
|
175
|
+
class_name, _ = ref.qualname.split(".", 1)
|
|
176
|
+
imports.add(f"from {ref.modulename} import {class_name}")
|
|
177
|
+
else:
|
|
178
|
+
imports.add(f"from {ref.modulename} import {ref.qualname}")
|
|
179
|
+
return imports
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def output_pytest_paths(
|
|
183
|
+
fn: Callable, paths: List[PathSummary]
|
|
184
|
+
) -> Tuple[Set[str], List[str]]:
|
|
185
|
+
fn_name = fn.__qualname__
|
|
186
|
+
lines: List[str] = []
|
|
187
|
+
references = {ReferencedIdentifier(fn.__module__, fn_name)}
|
|
188
|
+
imports: Set[str] = set()
|
|
189
|
+
|
|
190
|
+
name_with_underscores = fn_name.replace(".", "_")
|
|
191
|
+
for idx, path in enumerate(paths):
|
|
192
|
+
test_name_suffix = "" if idx == 0 else "_" + str(idx + 1)
|
|
193
|
+
exec_fn = f"{fn_name}({path.formatted_args})"
|
|
194
|
+
lines.append(f"def test_{name_with_underscores}{test_name_suffix}():")
|
|
195
|
+
if path.exc is None:
|
|
196
|
+
lines.append(f" assert {exec_fn} == {path.result}")
|
|
197
|
+
else:
|
|
198
|
+
imports.add("import pytest")
|
|
199
|
+
if path.exc_message is not None:
|
|
200
|
+
lines.append(
|
|
201
|
+
f" with pytest.raises({name_of_type(path.exc)}, match={repr(re.escape(path.exc_message))}):"
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
lines.append(f" with pytest.raises({name_of_type(path.exc)}):")
|
|
205
|
+
lines.append(f" {exec_fn}")
|
|
206
|
+
lines.append("")
|
|
207
|
+
references |= path.references
|
|
208
|
+
imports |= import_statements_for_references(references)
|
|
209
|
+
return (imports, lines)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import re
|
|
3
|
+
import textwrap
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from typing import Callable, Optional
|
|
8
|
+
|
|
9
|
+
from crosshair.fnutil import FunctionInfo
|
|
10
|
+
from crosshair.options import DEFAULT_OPTIONS
|
|
11
|
+
from crosshair.path_cover import (
|
|
12
|
+
CoverageType,
|
|
13
|
+
output_eval_exression_paths,
|
|
14
|
+
output_pytest_paths,
|
|
15
|
+
path_cover,
|
|
16
|
+
)
|
|
17
|
+
from crosshair.statespace import context_statespace
|
|
18
|
+
from crosshair.tracers import NoTracing
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _foo(x: int) -> int:
|
|
22
|
+
if x > 100:
|
|
23
|
+
return 100
|
|
24
|
+
return x
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _regex(x: str) -> bool:
|
|
28
|
+
compiled = re.compile("f(o)+")
|
|
29
|
+
return bool(compiled.fullmatch(x))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _exceptionex(x: int) -> int:
|
|
33
|
+
if x == 42:
|
|
34
|
+
raise ValueError
|
|
35
|
+
return x
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _symbolic_exception_example(x: str) -> str:
|
|
39
|
+
if x == "foo'bar\"baz":
|
|
40
|
+
raise ValueError(x)
|
|
41
|
+
return x
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _has_no_successful_paths(x: int) -> None:
|
|
45
|
+
with NoTracing():
|
|
46
|
+
context_statespace().defer_assumption("fail", lambda: False)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Train:
|
|
51
|
+
class Color(Enum):
|
|
52
|
+
RED = 0
|
|
53
|
+
|
|
54
|
+
color: Color
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _paint_train(train: Train, color: Train.Color) -> Train:
|
|
58
|
+
return Train(color=color)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
OPTS = DEFAULT_OPTIONS.overlay(max_iterations=10)
|
|
62
|
+
foo = FunctionInfo.from_fn(_foo)
|
|
63
|
+
decorated_foo = FunctionInfo.from_fn(functools.lru_cache()(_foo))
|
|
64
|
+
regex = FunctionInfo.from_fn(_regex)
|
|
65
|
+
exceptionex = FunctionInfo.from_fn(_exceptionex)
|
|
66
|
+
symbolic_exception_example = FunctionInfo.from_fn(_symbolic_exception_example)
|
|
67
|
+
has_no_successful_paths = FunctionInfo.from_fn(_has_no_successful_paths)
|
|
68
|
+
paint_train = FunctionInfo.from_fn(_paint_train)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_path_cover_foo() -> None:
|
|
72
|
+
paths = list(path_cover(foo, OPTS, CoverageType.OPCODE))
|
|
73
|
+
assert len(paths) == 2
|
|
74
|
+
small, large = sorted(paths, key=lambda p: p.result) # type: ignore
|
|
75
|
+
assert large.result == "100"
|
|
76
|
+
assert large.args.arguments["x"] > 100
|
|
77
|
+
assert small.result == repr(small.args.arguments["x"])
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_path_cover_decorated_foo() -> None:
|
|
81
|
+
paths = list(path_cover(decorated_foo, OPTS, CoverageType.OPCODE))
|
|
82
|
+
assert len(paths) == 2
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_path_cover_regex() -> None:
|
|
86
|
+
paths = list(path_cover(regex, OPTS, CoverageType.OPCODE))
|
|
87
|
+
assert len(paths) == 1
|
|
88
|
+
paths = list(path_cover(regex, OPTS, CoverageType.PATH))
|
|
89
|
+
input_output = set((p.args.arguments["x"], p.result) for p in paths)
|
|
90
|
+
assert ("foo", "True") in input_output
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_path_cover_exception_example() -> None:
|
|
94
|
+
paths = list(path_cover(exceptionex, OPTS, CoverageType.OPCODE))
|
|
95
|
+
out, err = StringIO(), StringIO()
|
|
96
|
+
output_eval_exression_paths(_exceptionex, paths, out, err)
|
|
97
|
+
assert "_exceptionex(42)" in out.getvalue()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_path_cover_symbolic_exception_message() -> None:
|
|
101
|
+
paths = list(path_cover(symbolic_exception_example, OPTS, CoverageType.OPCODE))
|
|
102
|
+
_imports, lines = output_pytest_paths(_symbolic_exception_example, paths)
|
|
103
|
+
expected = textwrap.dedent(
|
|
104
|
+
"""\
|
|
105
|
+
def test__symbolic_exception_example():
|
|
106
|
+
with pytest.raises(ValueError, match='foo\\'bar"baz'):
|
|
107
|
+
_symbolic_exception_example('foo\\'bar"baz')"""
|
|
108
|
+
)
|
|
109
|
+
assert expected in "\n".join(lines)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_has_no_successful_paths() -> None:
|
|
113
|
+
assert list(path_cover(has_no_successful_paths, OPTS, CoverageType.OPCODE)) == []
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_path_cover_lambda() -> None:
|
|
117
|
+
def lambdaFn(a: Optional[Callable[[int], int]]):
|
|
118
|
+
if a:
|
|
119
|
+
return a(2) + 4
|
|
120
|
+
else:
|
|
121
|
+
return "hello"
|
|
122
|
+
|
|
123
|
+
assert path_cover(FunctionInfo.from_fn(lambdaFn), OPTS, CoverageType.OPCODE)
|
|
124
|
+
# TODO: more detailed assert?
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_path_cover_pytest_output() -> None:
|
|
128
|
+
paths = list(path_cover(paint_train, OPTS, CoverageType.OPCODE))
|
|
129
|
+
imports, lines = output_pytest_paths(_paint_train, paths)
|
|
130
|
+
assert lines == [
|
|
131
|
+
"def test__paint_train():",
|
|
132
|
+
" assert _paint_train(Train(Train.Color.RED), Train.Color.RED) == Train(color=Train.Color.RED)",
|
|
133
|
+
"",
|
|
134
|
+
]
|
|
135
|
+
assert imports == {
|
|
136
|
+
"from crosshair.path_cover_test import _paint_train",
|
|
137
|
+
"from crosshair.path_cover_test import Train",
|
|
138
|
+
}
|
crosshair/path_search.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import traceback
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from inspect import BoundArguments
|
|
5
|
+
from typing import Callable, Optional, Type
|
|
6
|
+
|
|
7
|
+
from crosshair.copyext import CopyMode, deepcopyext
|
|
8
|
+
from crosshair.core import ExceptionFilter, LazyCreationRepr, explore_paths
|
|
9
|
+
from crosshair.fnutil import FunctionInfo
|
|
10
|
+
from crosshair.libimpl.builtinslib import SymbolicInt
|
|
11
|
+
from crosshair.options import AnalysisOptions
|
|
12
|
+
from crosshair.statespace import RootNode, StateSpace, context_statespace
|
|
13
|
+
from crosshair.tracers import CoverageResult, NoTracing, ResumedTracing
|
|
14
|
+
from crosshair.util import (
|
|
15
|
+
CrossHairInternal,
|
|
16
|
+
ch_stack,
|
|
17
|
+
debug,
|
|
18
|
+
format_boundargs_as_dictionary,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OptimizationKind(enum.Enum):
|
|
23
|
+
SIMPLIFY = "SIMPLIFY"
|
|
24
|
+
# TODO: simplify mode is quite dumb atm; partly because eval_friendly_format
|
|
25
|
+
# realizes prior to generating the string.
|
|
26
|
+
NONE = "NONE"
|
|
27
|
+
MINIMIZE_INT = "MINIMIZE_INT"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class PathSummary:
|
|
32
|
+
args: BoundArguments
|
|
33
|
+
result: object
|
|
34
|
+
exc: Optional[Type[BaseException]]
|
|
35
|
+
post_args: BoundArguments
|
|
36
|
+
coverage: CoverageResult
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def path_search(
|
|
40
|
+
ctxfn: FunctionInfo,
|
|
41
|
+
options: AnalysisOptions,
|
|
42
|
+
argument_formatter: Optional[Callable[[BoundArguments], str]],
|
|
43
|
+
optimization_kind: OptimizationKind,
|
|
44
|
+
optimize_fn: Optional[Callable],
|
|
45
|
+
on_example: Callable[[str], None],
|
|
46
|
+
) -> None:
|
|
47
|
+
|
|
48
|
+
if argument_formatter is None:
|
|
49
|
+
checked_format = (
|
|
50
|
+
lambda args: context_statespace()
|
|
51
|
+
.extra(LazyCreationRepr)
|
|
52
|
+
.eval_friendly_format(args, format_boundargs_as_dictionary)
|
|
53
|
+
)
|
|
54
|
+
else:
|
|
55
|
+
|
|
56
|
+
def checked_format(args: BoundArguments) -> str:
|
|
57
|
+
assert argument_formatter is not None
|
|
58
|
+
args = deepcopyext(args, CopyMode.REALIZE, {})
|
|
59
|
+
try:
|
|
60
|
+
return argument_formatter(args)
|
|
61
|
+
except Exception as exc:
|
|
62
|
+
raise CrossHairInternal(str(exc)) from exc
|
|
63
|
+
|
|
64
|
+
if optimization_kind == OptimizationKind.SIMPLIFY:
|
|
65
|
+
assert optimize_fn is None
|
|
66
|
+
|
|
67
|
+
def scorechar(codepoint: int):
|
|
68
|
+
if codepoint >= ord("a"):
|
|
69
|
+
return codepoint - ord("a")
|
|
70
|
+
elif codepoint >= ord("0"):
|
|
71
|
+
return 30 + codepoint - ord("0")
|
|
72
|
+
else:
|
|
73
|
+
return 100 + codepoint
|
|
74
|
+
|
|
75
|
+
def shrinkscore(ret, args: BoundArguments):
|
|
76
|
+
reprstr = checked_format(args)
|
|
77
|
+
return len(reprstr) * 1000 + sum(scorechar(ord(ch)) for ch in reprstr)
|
|
78
|
+
|
|
79
|
+
optimization_kind = OptimizationKind.MINIMIZE_INT
|
|
80
|
+
optimize_fn = shrinkscore
|
|
81
|
+
|
|
82
|
+
fn, sig = ctxfn.callable()
|
|
83
|
+
search_root = RootNode()
|
|
84
|
+
|
|
85
|
+
best_input: Optional[str] = None
|
|
86
|
+
best_score: Optional[int] = None
|
|
87
|
+
_optimize_fn = optimize_fn or (lambda _ret, args: fn(*args.args, **args.kwargs))
|
|
88
|
+
|
|
89
|
+
def on_path_complete(
|
|
90
|
+
space: StateSpace,
|
|
91
|
+
pre_args: BoundArguments,
|
|
92
|
+
post_args: BoundArguments,
|
|
93
|
+
ret,
|
|
94
|
+
exc: Optional[BaseException],
|
|
95
|
+
exc_stack: Optional[traceback.StackSummary],
|
|
96
|
+
) -> bool:
|
|
97
|
+
nonlocal best_input, best_score
|
|
98
|
+
with NoTracing():
|
|
99
|
+
if exc is not None:
|
|
100
|
+
debug(
|
|
101
|
+
"Aborting path, hit exception",
|
|
102
|
+
type(exc),
|
|
103
|
+
exc,
|
|
104
|
+
ch_stack(exc_stack),
|
|
105
|
+
)
|
|
106
|
+
return False
|
|
107
|
+
debug("Path succeeded")
|
|
108
|
+
if optimization_kind == OptimizationKind.NONE:
|
|
109
|
+
with ResumedTracing():
|
|
110
|
+
best_input = checked_format(pre_args)
|
|
111
|
+
debug("Found input:", best_input)
|
|
112
|
+
on_example(best_input)
|
|
113
|
+
return True
|
|
114
|
+
with NoTracing(), ExceptionFilter() as efilter:
|
|
115
|
+
with ResumedTracing():
|
|
116
|
+
score = _optimize_fn(ret, pre_args)
|
|
117
|
+
space.detach_path()
|
|
118
|
+
smt_score = SymbolicInt._coerce_to_smt_sort(score)
|
|
119
|
+
if smt_score is None:
|
|
120
|
+
debug("non integer score returned; ignoring.")
|
|
121
|
+
return False
|
|
122
|
+
if space.smt_fork(smt_score < 0, probability_true=0.0):
|
|
123
|
+
debug("Score was leass than zero; ignoring.")
|
|
124
|
+
return False
|
|
125
|
+
if best_score is not None and space.smt_fork(
|
|
126
|
+
smt_score >= best_score, probability_true=0.0
|
|
127
|
+
):
|
|
128
|
+
debug("Does not beat the best score of", best_score)
|
|
129
|
+
return False
|
|
130
|
+
known_min = 0
|
|
131
|
+
known_max = best_score
|
|
132
|
+
test = 1000 if known_max is None else (known_max // 2)
|
|
133
|
+
while True:
|
|
134
|
+
debug(known_min, test, known_max)
|
|
135
|
+
if space.smt_fork(smt_score < test, probability_true=1.0):
|
|
136
|
+
known_max = test - 1
|
|
137
|
+
else:
|
|
138
|
+
known_min = test
|
|
139
|
+
if known_max is None:
|
|
140
|
+
test = known_min * 100
|
|
141
|
+
continue
|
|
142
|
+
if known_min == known_max:
|
|
143
|
+
best_score = known_min
|
|
144
|
+
with ResumedTracing():
|
|
145
|
+
best_input = checked_format(pre_args)
|
|
146
|
+
break
|
|
147
|
+
test = (known_min + known_max + 1) // 2
|
|
148
|
+
debug("Minimized score to", best_score)
|
|
149
|
+
debug("For input:", best_input)
|
|
150
|
+
on_example(best_input)
|
|
151
|
+
return best_score == 0
|
|
152
|
+
debug("Skipping path (failure during scoring)", efilter.user_exc)
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
explore_paths(
|
|
156
|
+
lambda ba: fn(*ba.args, **ba.kwargs),
|
|
157
|
+
sig,
|
|
158
|
+
options,
|
|
159
|
+
search_root,
|
|
160
|
+
on_path_complete,
|
|
161
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from inspect import BoundArguments
|
|
3
|
+
from typing import Callable, Optional
|
|
4
|
+
|
|
5
|
+
from crosshair.fnutil import FunctionInfo
|
|
6
|
+
from crosshair.options import DEFAULT_OPTIONS, AnalysisOptions, AnalysisOptionSet
|
|
7
|
+
from crosshair.path_search import OptimizationKind, path_search
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def ten_over_difference(x: int, y: int) -> int:
|
|
11
|
+
if x != 42:
|
|
12
|
+
return 10 // (x - y)
|
|
13
|
+
return 100
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def do_path_search(
|
|
17
|
+
fn: Callable,
|
|
18
|
+
options: AnalysisOptions,
|
|
19
|
+
argument_formatter: Optional[Callable[[BoundArguments], str]],
|
|
20
|
+
optimization_kind: OptimizationKind,
|
|
21
|
+
optimize_fn: Optional[Callable] = None,
|
|
22
|
+
) -> Optional[str]:
|
|
23
|
+
fninfo = FunctionInfo.from_fn(fn)
|
|
24
|
+
final_example: Optional[str] = None
|
|
25
|
+
|
|
26
|
+
def on_example(example: str) -> None:
|
|
27
|
+
nonlocal final_example
|
|
28
|
+
final_example = example
|
|
29
|
+
|
|
30
|
+
path_search(
|
|
31
|
+
fninfo, options, argument_formatter, optimization_kind, optimize_fn, on_example
|
|
32
|
+
)
|
|
33
|
+
return final_example
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_optimize_options() -> None:
|
|
37
|
+
opts = DEFAULT_OPTIONS.overlay(AnalysisOptionSet(max_uninteresting_iterations=10))
|
|
38
|
+
ret = do_path_search(
|
|
39
|
+
ten_over_difference, opts, None, optimization_kind=OptimizationKind.SIMPLIFY
|
|
40
|
+
)
|
|
41
|
+
assert ret in ('{"x": 1, "y": 0}', '{"x": 0, "y": 1}')
|
|
42
|
+
ret = do_path_search(
|
|
43
|
+
ten_over_difference, opts, None, optimization_kind=OptimizationKind.MINIMIZE_INT
|
|
44
|
+
)
|
|
45
|
+
assert ret is not None
|
|
46
|
+
parsed_ret = ast.literal_eval(ret)
|
|
47
|
+
assert parsed_ret["x"] - parsed_ret["y"] > 10
|
|
48
|
+
ret = do_path_search(
|
|
49
|
+
ten_over_difference, opts, None, optimization_kind=OptimizationKind.NONE
|
|
50
|
+
)
|
|
51
|
+
assert ret is not None
|
|
52
|
+
ast.literal_eval(ret) # (just ensure the result is parseable)
|