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/main.py
ADDED
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import enum
|
|
3
|
+
import linecache
|
|
4
|
+
import os.path
|
|
5
|
+
import random
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
import textwrap
|
|
9
|
+
import time
|
|
10
|
+
import traceback
|
|
11
|
+
from collections import deque
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from types import ModuleType
|
|
14
|
+
from typing import (
|
|
15
|
+
Callable,
|
|
16
|
+
Counter,
|
|
17
|
+
Dict,
|
|
18
|
+
Iterable,
|
|
19
|
+
List,
|
|
20
|
+
MutableMapping,
|
|
21
|
+
NoReturn,
|
|
22
|
+
Optional,
|
|
23
|
+
Sequence,
|
|
24
|
+
TextIO,
|
|
25
|
+
Tuple,
|
|
26
|
+
Union,
|
|
27
|
+
cast,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from crosshair import env_info
|
|
31
|
+
from crosshair.auditwall import disable_auditwall, engage_auditwall
|
|
32
|
+
from crosshair.core_and_libs import (
|
|
33
|
+
AnalysisMessage,
|
|
34
|
+
MessageType,
|
|
35
|
+
analyze_any,
|
|
36
|
+
installed_plugins,
|
|
37
|
+
run_checkables,
|
|
38
|
+
)
|
|
39
|
+
from crosshair.diff_behavior import ExceptionEquivalenceType, diff_behavior
|
|
40
|
+
from crosshair.fnutil import (
|
|
41
|
+
FUNCTIONINFO_DESCRIPTOR_TYPES,
|
|
42
|
+
FunctionInfo,
|
|
43
|
+
get_top_level_classes_and_functions,
|
|
44
|
+
load_files_or_qualnames,
|
|
45
|
+
)
|
|
46
|
+
from crosshair.options import (
|
|
47
|
+
DEFAULT_OPTIONS,
|
|
48
|
+
AnalysisKind,
|
|
49
|
+
AnalysisOptions,
|
|
50
|
+
AnalysisOptionSet,
|
|
51
|
+
option_set_from_dict,
|
|
52
|
+
)
|
|
53
|
+
from crosshair.path_cover import (
|
|
54
|
+
CoverageType,
|
|
55
|
+
output_argument_dictionary_paths,
|
|
56
|
+
output_eval_exression_paths,
|
|
57
|
+
output_pytest_paths,
|
|
58
|
+
path_cover,
|
|
59
|
+
)
|
|
60
|
+
from crosshair.path_search import OptimizationKind, path_search
|
|
61
|
+
from crosshair.pure_importer import prefer_pure_python_imports
|
|
62
|
+
from crosshair.register_contract import REGISTERED_CONTRACTS
|
|
63
|
+
from crosshair.util import (
|
|
64
|
+
ErrorDuringImport,
|
|
65
|
+
NotDeterministic,
|
|
66
|
+
add_to_pypath,
|
|
67
|
+
debug,
|
|
68
|
+
format_boundargs,
|
|
69
|
+
format_boundargs_as_dictionary,
|
|
70
|
+
in_debug,
|
|
71
|
+
set_debug,
|
|
72
|
+
)
|
|
73
|
+
from crosshair.watcher import Watcher
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ExampleOutputFormat(enum.Enum):
|
|
77
|
+
ARGUMENT_DICTIONARY = "ARGUMENT_DICTIONARY" # deprecated
|
|
78
|
+
ARG_DICTIONARY = "ARG_DICTIONARY"
|
|
79
|
+
EVAL_EXPRESSION = "EVAL_EXPRESSION"
|
|
80
|
+
PYTEST = "PYTEST"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def analysis_kind(argstr: str) -> Sequence[AnalysisKind]:
|
|
84
|
+
try:
|
|
85
|
+
ret = [AnalysisKind[part.strip()] for part in argstr.split(",")]
|
|
86
|
+
except KeyError:
|
|
87
|
+
raise ValueError
|
|
88
|
+
return ret
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def command_line_parser() -> argparse.ArgumentParser:
|
|
92
|
+
common = argparse.ArgumentParser(
|
|
93
|
+
add_help=False, formatter_class=argparse.RawTextHelpFormatter
|
|
94
|
+
)
|
|
95
|
+
common.add_argument(
|
|
96
|
+
"--verbose",
|
|
97
|
+
"-v",
|
|
98
|
+
action="store_true",
|
|
99
|
+
help="Output additional debugging information on stderr",
|
|
100
|
+
)
|
|
101
|
+
common.add_argument(
|
|
102
|
+
"--extra_plugin",
|
|
103
|
+
type=str,
|
|
104
|
+
nargs="+",
|
|
105
|
+
help="Plugin file(s) you wish to use during the current execution",
|
|
106
|
+
)
|
|
107
|
+
parser = argparse.ArgumentParser(
|
|
108
|
+
prog="crosshair", description="CrossHair Analysis Tool"
|
|
109
|
+
)
|
|
110
|
+
subparsers = parser.add_subparsers(help="sub-command help", dest="action")
|
|
111
|
+
check_parser = subparsers.add_parser(
|
|
112
|
+
"check",
|
|
113
|
+
help="Analyze a file or function",
|
|
114
|
+
parents=[common],
|
|
115
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
116
|
+
description=textwrap.dedent(
|
|
117
|
+
"""\
|
|
118
|
+
The check command looks for counterexamples that break contracts.
|
|
119
|
+
|
|
120
|
+
It outputs machine-readable messages in this format on stdout:
|
|
121
|
+
<filename>:<line number>: error: <error message>
|
|
122
|
+
|
|
123
|
+
It exits with one of the following codes:
|
|
124
|
+
0 : No counterexamples are found
|
|
125
|
+
1 : Counterexample(s) have been found
|
|
126
|
+
2 : Other error
|
|
127
|
+
"""
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
check_parser.add_argument(
|
|
131
|
+
"--report_all",
|
|
132
|
+
action="store_true",
|
|
133
|
+
help="Output analysis results for all postconditions (not just failing ones)",
|
|
134
|
+
)
|
|
135
|
+
check_parser.add_argument(
|
|
136
|
+
"--report_verbose",
|
|
137
|
+
dest="report_verbose",
|
|
138
|
+
action="store_true",
|
|
139
|
+
help="Output context and stack traces for counterexamples",
|
|
140
|
+
)
|
|
141
|
+
check_parser.add_argument(
|
|
142
|
+
"target",
|
|
143
|
+
metavar="TARGET",
|
|
144
|
+
type=str,
|
|
145
|
+
nargs="+",
|
|
146
|
+
help=textwrap.dedent(
|
|
147
|
+
"""\
|
|
148
|
+
A fully qualified module, class, or function, or
|
|
149
|
+
a directory (which will be recursively analyzed), or
|
|
150
|
+
a file path with an optional ":<line-number>" suffix.
|
|
151
|
+
See https://crosshair.readthedocs.io/en/latest/contracts.html#targeting
|
|
152
|
+
"""
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
search_parser = subparsers.add_parser(
|
|
156
|
+
"search",
|
|
157
|
+
help="Find arguments to make a function complete without error",
|
|
158
|
+
parents=[common],
|
|
159
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
160
|
+
description=textwrap.dedent(
|
|
161
|
+
"""\
|
|
162
|
+
The search command finds arguments for a function that causes it to
|
|
163
|
+
complete without error.
|
|
164
|
+
|
|
165
|
+
Results (if any) are written to stdout in the form of a repr'd
|
|
166
|
+
dictionary, mapping argument names to values.
|
|
167
|
+
"""
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
search_parser.add_argument(
|
|
171
|
+
"fn",
|
|
172
|
+
metavar="FUNCTION",
|
|
173
|
+
type=str,
|
|
174
|
+
help='A fully-qualified function to explore (e.g. "mymodule.myfunc")',
|
|
175
|
+
)
|
|
176
|
+
search_parser.add_argument(
|
|
177
|
+
"--optimization",
|
|
178
|
+
type=lambda e: OptimizationKind[e.upper()], # type: ignore
|
|
179
|
+
choices=OptimizationKind.__members__.values(),
|
|
180
|
+
metavar="OPTIMIZATION_TYPE",
|
|
181
|
+
default=OptimizationKind.SIMPLIFY,
|
|
182
|
+
help=textwrap.dedent(
|
|
183
|
+
"""\
|
|
184
|
+
Controls what kind of arguments are produced.
|
|
185
|
+
Optimization effectiveness will vary wildly depnding on the nature of
|
|
186
|
+
the function.
|
|
187
|
+
simplify : [default] Attempt to minimize the size
|
|
188
|
+
(in characters) of the arguments.
|
|
189
|
+
none : Output the first set of arguments found.
|
|
190
|
+
minimize_int : Attempt to minimize an integer returned by the
|
|
191
|
+
function. Negative return values are ignored.
|
|
192
|
+
"""
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
search_parser.add_argument(
|
|
196
|
+
"--output_all_examples",
|
|
197
|
+
action="store_true",
|
|
198
|
+
default=False,
|
|
199
|
+
help=textwrap.dedent(
|
|
200
|
+
"""\
|
|
201
|
+
When optimizing, output an example every time a new best score is discovered.
|
|
202
|
+
"""
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
search_parser.add_argument(
|
|
206
|
+
"--argument_formatter",
|
|
207
|
+
metavar="FUNCTION",
|
|
208
|
+
type=str,
|
|
209
|
+
help=textwrap.dedent(
|
|
210
|
+
"""\
|
|
211
|
+
The (fully-qualified) name of a function for formatting produced
|
|
212
|
+
arguments. If specified, crosshair will call this function instead of
|
|
213
|
+
repr() when printing arguments to stdout.
|
|
214
|
+
Your formatting function will be pased an `inspect.BoundArguments`
|
|
215
|
+
instance. It should return a string.
|
|
216
|
+
"""
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
watch_parser = subparsers.add_parser(
|
|
221
|
+
"watch",
|
|
222
|
+
help="Continuously watch and analyze a directory",
|
|
223
|
+
parents=[common],
|
|
224
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
225
|
+
description=textwrap.dedent(
|
|
226
|
+
"""\
|
|
227
|
+
The watch command continuously looks for contract counterexamples.
|
|
228
|
+
Type Ctrl-C to stop this command.
|
|
229
|
+
"""
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
watch_parser.add_argument(
|
|
233
|
+
"directory",
|
|
234
|
+
metavar="TARGET",
|
|
235
|
+
type=str,
|
|
236
|
+
nargs="+",
|
|
237
|
+
help=textwrap.dedent(
|
|
238
|
+
"""\
|
|
239
|
+
File or directory to watch. Directories will be recursively analyzed.
|
|
240
|
+
See https://crosshair.readthedocs.io/en/latest/contracts.html#targeting
|
|
241
|
+
"""
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
diffbehavior_parser = subparsers.add_parser(
|
|
245
|
+
"diffbehavior",
|
|
246
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
247
|
+
help="Find differences in the behavior of two functions",
|
|
248
|
+
description=textwrap.dedent(
|
|
249
|
+
"""\
|
|
250
|
+
Find differences in the behavior of two functions.
|
|
251
|
+
See https://crosshair.readthedocs.io/en/latest/diff_behavior.html
|
|
252
|
+
"""
|
|
253
|
+
),
|
|
254
|
+
parents=[common],
|
|
255
|
+
)
|
|
256
|
+
diffbehavior_parser.add_argument(
|
|
257
|
+
"fn1",
|
|
258
|
+
metavar="FUNCTION1",
|
|
259
|
+
type=str,
|
|
260
|
+
help='first fully-qualified function to compare (e.g. "mymodule.myfunc")',
|
|
261
|
+
)
|
|
262
|
+
diffbehavior_parser.add_argument(
|
|
263
|
+
"fn2",
|
|
264
|
+
metavar="FUNCTION2",
|
|
265
|
+
type=str,
|
|
266
|
+
help="second fully-qualified function to compare",
|
|
267
|
+
)
|
|
268
|
+
diffbehavior_parser.add_argument(
|
|
269
|
+
"--exception_equivalence",
|
|
270
|
+
metavar="EXCEPTION_EQUIVALENCE",
|
|
271
|
+
type=ExceptionEquivalenceType,
|
|
272
|
+
default=ExceptionEquivalenceType.TYPE_AND_MESSAGE,
|
|
273
|
+
choices=ExceptionEquivalenceType.__members__.values(),
|
|
274
|
+
help=textwrap.dedent(
|
|
275
|
+
"""\
|
|
276
|
+
Decide how to treat exceptions, while searching for a counter-example.
|
|
277
|
+
`ALL` treats all exceptions as equivalent,
|
|
278
|
+
`SAME_TYPE`, considers matches on the type.
|
|
279
|
+
`TYPE_AND_MESSAGE` matches for the same type and message.
|
|
280
|
+
"""
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
cover_parser = subparsers.add_parser(
|
|
284
|
+
"cover",
|
|
285
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
286
|
+
help="Generate inputs for a function, attempting to exercise different code paths",
|
|
287
|
+
description=textwrap.dedent(
|
|
288
|
+
"""\
|
|
289
|
+
Generates inputs to a function, hopefully getting good line, branch,
|
|
290
|
+
and path coverage.
|
|
291
|
+
See https://crosshair.readthedocs.io/en/latest/cover.html
|
|
292
|
+
"""
|
|
293
|
+
),
|
|
294
|
+
parents=[common],
|
|
295
|
+
)
|
|
296
|
+
cover_parser.add_argument(
|
|
297
|
+
"target",
|
|
298
|
+
metavar="TARGET",
|
|
299
|
+
type=str,
|
|
300
|
+
nargs="+",
|
|
301
|
+
help=textwrap.dedent(
|
|
302
|
+
"""\
|
|
303
|
+
A fully qualified module, class, or function, or
|
|
304
|
+
a directory (which will be recursively analyzed), or
|
|
305
|
+
a file path with an optional ":<line-number>" suffix.
|
|
306
|
+
See https://crosshair.readthedocs.io/en/latest/contracts.html#targeting
|
|
307
|
+
"""
|
|
308
|
+
),
|
|
309
|
+
)
|
|
310
|
+
cover_parser.add_argument(
|
|
311
|
+
"--example_output_format",
|
|
312
|
+
type=lambda e: ExampleOutputFormat[e.upper()], # type: ignore
|
|
313
|
+
choices=ExampleOutputFormat.__members__.values(),
|
|
314
|
+
metavar="FORMAT",
|
|
315
|
+
default=ExampleOutputFormat.EVAL_EXPRESSION,
|
|
316
|
+
help=textwrap.dedent(
|
|
317
|
+
"""\
|
|
318
|
+
Determines how to output examples.
|
|
319
|
+
eval_expression : [default] Output examples as expressions,
|
|
320
|
+
suitable for eval()
|
|
321
|
+
arg_dictionary : Output arguments as repr'd, ordered
|
|
322
|
+
dictionaries
|
|
323
|
+
pytest : Output examples as stub pytest tests
|
|
324
|
+
argument_dictionary : Deprecated
|
|
325
|
+
"""
|
|
326
|
+
),
|
|
327
|
+
)
|
|
328
|
+
cover_parser.add_argument(
|
|
329
|
+
"--coverage_type",
|
|
330
|
+
type=lambda e: CoverageType[e.upper()], # type: ignore
|
|
331
|
+
choices=CoverageType.__members__.values(),
|
|
332
|
+
metavar="TYPE",
|
|
333
|
+
default=CoverageType.OPCODE,
|
|
334
|
+
help=textwrap.dedent(
|
|
335
|
+
"""\
|
|
336
|
+
Determines what kind of coverage to achieve.
|
|
337
|
+
opcode : [default] Cover as many opcodes of the function as
|
|
338
|
+
possible. This is similar to "branch" coverage.
|
|
339
|
+
path : Cover any possible execution path.
|
|
340
|
+
There will usually be an infinite number of paths (e.g.
|
|
341
|
+
loops are effectively unrolled). Use
|
|
342
|
+
max_uninteresting_iterations and/or per_condition_timeout
|
|
343
|
+
to bound results.
|
|
344
|
+
Many path decisions are internal to CrossHair, so you may
|
|
345
|
+
see more duplicative-ness in the output than you'd expect.
|
|
346
|
+
"""
|
|
347
|
+
),
|
|
348
|
+
)
|
|
349
|
+
for subparser in (check_parser, search_parser, diffbehavior_parser, cover_parser):
|
|
350
|
+
subparser.add_argument(
|
|
351
|
+
"--max_uninteresting_iterations",
|
|
352
|
+
type=int,
|
|
353
|
+
help=textwrap.dedent(
|
|
354
|
+
"""\
|
|
355
|
+
Maximum number of consecutive iterations to run without making
|
|
356
|
+
significant progress in exploring the codebase.
|
|
357
|
+
(by default, 5 iterations, unless --per_condition_timeout is set)
|
|
358
|
+
|
|
359
|
+
This option can be more useful than --per_condition_timeout
|
|
360
|
+
because the amount of time invested will scale with the complexity
|
|
361
|
+
of the code under analysis.
|
|
362
|
+
|
|
363
|
+
Use a small integer (3-5) for fast but weak analysis.
|
|
364
|
+
Values in the hundreds or thousands may be appropriate if you
|
|
365
|
+
intend to run CrossHair for hours.
|
|
366
|
+
"""
|
|
367
|
+
),
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
for subparser in (check_parser, search_parser, diffbehavior_parser, cover_parser):
|
|
371
|
+
subparser.add_argument(
|
|
372
|
+
"--per_path_timeout",
|
|
373
|
+
type=float,
|
|
374
|
+
metavar="FLOAT",
|
|
375
|
+
help=textwrap.dedent(
|
|
376
|
+
"""\
|
|
377
|
+
Maximum seconds to spend checking one execution path.
|
|
378
|
+
If unspecified:
|
|
379
|
+
1. CrossHair will timeout each path at the square root of
|
|
380
|
+
`--per_condition_timeout`, if specified.
|
|
381
|
+
3. Otherwise, it will timeout each path at a number of seconds
|
|
382
|
+
equal to `--max_uninteresting_iterations`, unless it is
|
|
383
|
+
explicitly set to zero.
|
|
384
|
+
(NOTE: `--max_uninteresting_iterations` is 5 by default)
|
|
385
|
+
2. Otherwise, it will not use any per-path timeout.
|
|
386
|
+
"""
|
|
387
|
+
),
|
|
388
|
+
)
|
|
389
|
+
subparser.add_argument(
|
|
390
|
+
"--per_condition_timeout",
|
|
391
|
+
type=float,
|
|
392
|
+
metavar="FLOAT",
|
|
393
|
+
help="Maximum seconds to spend checking execution paths for one condition",
|
|
394
|
+
)
|
|
395
|
+
lsp_server_parser = subparsers.add_parser(
|
|
396
|
+
"server",
|
|
397
|
+
help="Start a server, speaking the Language Server Protocol",
|
|
398
|
+
parents=[common],
|
|
399
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
400
|
+
description=textwrap.dedent(
|
|
401
|
+
f"""\
|
|
402
|
+
Many IDEs support the Language Server Protocol (LSP).
|
|
403
|
+
CrossHair can produce various results and analysis through LSP.
|
|
404
|
+
"""
|
|
405
|
+
),
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
for subparser in (check_parser, watch_parser, lsp_server_parser):
|
|
409
|
+
subparser.add_argument(
|
|
410
|
+
"--analysis_kind",
|
|
411
|
+
type=analysis_kind,
|
|
412
|
+
metavar="KIND",
|
|
413
|
+
help=textwrap.dedent(
|
|
414
|
+
"""\
|
|
415
|
+
Kind of contract to check.
|
|
416
|
+
By default, the PEP316, deal, and icontract kinds are all checked.
|
|
417
|
+
Multiple kinds (comma-separated) may be given.
|
|
418
|
+
See https://crosshair.readthedocs.io/en/latest/kinds_of_contracts.html
|
|
419
|
+
asserts : check assert statements
|
|
420
|
+
PEP316 : check PEP316 contracts (docstring-based)
|
|
421
|
+
icontract : check icontract contracts (decorator-based)
|
|
422
|
+
deal : check deal contracts (decorator-based)
|
|
423
|
+
"""
|
|
424
|
+
),
|
|
425
|
+
)
|
|
426
|
+
return parser
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def run_watch_loop(
|
|
430
|
+
watcher: Watcher,
|
|
431
|
+
max_watch_iterations: int = sys.maxsize,
|
|
432
|
+
term_lines_rewritable: bool = True,
|
|
433
|
+
) -> None:
|
|
434
|
+
restart = True
|
|
435
|
+
stats: Counter[str] = Counter()
|
|
436
|
+
active_messages: Dict[Tuple[str, int], AnalysisMessage]
|
|
437
|
+
for _ in range(max_watch_iterations):
|
|
438
|
+
if restart:
|
|
439
|
+
clear_screen()
|
|
440
|
+
print_divider("-")
|
|
441
|
+
line = f" Analyzing {len(watcher._modtimes)} files."
|
|
442
|
+
print(color(line, AnsiColor.OKBLUE), end="")
|
|
443
|
+
max_uninteresting_iterations = (
|
|
444
|
+
DEFAULT_OPTIONS.get_max_uninteresting_iterations()
|
|
445
|
+
)
|
|
446
|
+
restart = False
|
|
447
|
+
stats = Counter()
|
|
448
|
+
active_messages = {}
|
|
449
|
+
else:
|
|
450
|
+
time.sleep(0.1)
|
|
451
|
+
max_uninteresting_iterations = min(
|
|
452
|
+
max_uninteresting_iterations * 2, 100_000_000
|
|
453
|
+
)
|
|
454
|
+
for curstats, messages in watcher.run_iteration(max_uninteresting_iterations):
|
|
455
|
+
messages = [m for m in messages if m.state > MessageType.PRE_UNSAT]
|
|
456
|
+
stats.update(curstats)
|
|
457
|
+
if messages_merged(active_messages, messages):
|
|
458
|
+
linecache.checkcache()
|
|
459
|
+
clear_screen()
|
|
460
|
+
options = DEFAULT_OPTIONS.overlay(watcher._options)
|
|
461
|
+
for message in active_messages.values():
|
|
462
|
+
lines = long_describe_message(message, options)
|
|
463
|
+
if lines is None:
|
|
464
|
+
continue
|
|
465
|
+
print_divider("-")
|
|
466
|
+
print(lines, end="")
|
|
467
|
+
print_divider("-")
|
|
468
|
+
else:
|
|
469
|
+
if term_lines_rewritable:
|
|
470
|
+
print("\r", end="")
|
|
471
|
+
else:
|
|
472
|
+
print(".", end="")
|
|
473
|
+
continue
|
|
474
|
+
num_files = len(watcher._modtimes)
|
|
475
|
+
if len(watcher._paths) > 1:
|
|
476
|
+
loc_desc = f"{num_files} files"
|
|
477
|
+
else:
|
|
478
|
+
path_parts = Path(next(iter(watcher._paths))).parts
|
|
479
|
+
path_desc = path_parts[-1] if path_parts else "."
|
|
480
|
+
if num_files > 1:
|
|
481
|
+
loc_desc = f'"{path_desc}" ({num_files} files)'
|
|
482
|
+
else:
|
|
483
|
+
loc_desc = f'"{path_desc}"'
|
|
484
|
+
if term_lines_rewritable:
|
|
485
|
+
line = f' Analyzed {stats["num_paths"]} paths in {loc_desc}. '
|
|
486
|
+
else:
|
|
487
|
+
line = f" Analyzing paths in {loc_desc}: "
|
|
488
|
+
print(color(line, AnsiColor.OKBLUE), end="")
|
|
489
|
+
if watcher._change_flag:
|
|
490
|
+
watcher._change_flag = False
|
|
491
|
+
restart = True
|
|
492
|
+
line = f" Restarting analysis over {len(watcher._modtimes)} files."
|
|
493
|
+
print(color(line, AnsiColor.OKBLUE), end="")
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def clear_screen():
|
|
497
|
+
# Print enough newlines to fill the screen:
|
|
498
|
+
print("\n" * shutil.get_terminal_size().lines, end="")
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def print_divider(ch=" "):
|
|
502
|
+
try:
|
|
503
|
+
cols = os.get_terminal_size().columns - 1
|
|
504
|
+
except OSError:
|
|
505
|
+
cols = 5
|
|
506
|
+
print(ch * cols)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
class AnsiColor(enum.Enum):
|
|
510
|
+
HEADER = "\033[95m"
|
|
511
|
+
OKBLUE = "\033[94m"
|
|
512
|
+
OKGREEN = "\033[92m"
|
|
513
|
+
WARNING = "\033[93m"
|
|
514
|
+
FAIL = "\033[91m"
|
|
515
|
+
ENDC = "\033[0m"
|
|
516
|
+
BOLD = "\033[1m"
|
|
517
|
+
UNDERLINE = "\033[4m"
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def color(text: str, *effects: AnsiColor) -> str:
|
|
521
|
+
return "".join(e.value for e in effects) + text + AnsiColor.ENDC.value
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def messages_merged(
|
|
525
|
+
messages: MutableMapping[Tuple[str, int], AnalysisMessage],
|
|
526
|
+
new_messages: Iterable[AnalysisMessage],
|
|
527
|
+
) -> bool:
|
|
528
|
+
any_change = False
|
|
529
|
+
for message in new_messages:
|
|
530
|
+
key = (message.filename, message.line)
|
|
531
|
+
if key not in messages:
|
|
532
|
+
messages[key] = message
|
|
533
|
+
any_change = True
|
|
534
|
+
return any_change
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
_MOTD = [
|
|
538
|
+
"Did I miss a counterexample? Let me know: https://github.com/pschanely/CrossHair/issues/new",
|
|
539
|
+
"Help me be faster! Add to my benchmark suite: https://github.com/pschanely/crosshair-benchmark",
|
|
540
|
+
"Please consider sharing your CrossHair experience with others on social media.",
|
|
541
|
+
"Questions? Ask at https://github.com/pschanely/CrossHair/discussions/new?category=q-a",
|
|
542
|
+
"Consider signing up for CrossHair updates at https://pschanely.github.io",
|
|
543
|
+
"Come say hello in the discord chat; we are friendly! https://discord.gg/rUeTaYTWbb",
|
|
544
|
+
]
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def watch(
|
|
548
|
+
args: argparse.Namespace,
|
|
549
|
+
options: AnalysisOptionSet,
|
|
550
|
+
max_watch_iterations=sys.maxsize,
|
|
551
|
+
) -> int:
|
|
552
|
+
if not args.directory:
|
|
553
|
+
print("No files or directories given to watch", file=sys.stderr)
|
|
554
|
+
return 2
|
|
555
|
+
try:
|
|
556
|
+
paths = [Path(d) for d in args.directory]
|
|
557
|
+
|
|
558
|
+
# While the watcher is tolerant of files and directories disappearing mid-run,
|
|
559
|
+
# we still expect them to exist at launch time to make typos obvious:
|
|
560
|
+
nonexistent = [p for p in paths if not p.exists()]
|
|
561
|
+
if nonexistent:
|
|
562
|
+
print(
|
|
563
|
+
f"File(s) not found: {', '.join(map(str, nonexistent))}",
|
|
564
|
+
file=sys.stderr,
|
|
565
|
+
)
|
|
566
|
+
return 2
|
|
567
|
+
|
|
568
|
+
watcher = Watcher(paths, options)
|
|
569
|
+
watcher.check_changed()
|
|
570
|
+
|
|
571
|
+
# Some terminals don't interpret \r correctly; we detect them here:
|
|
572
|
+
term_lines_rewritable = "THONNY_USER_DIR" not in os.environ
|
|
573
|
+
|
|
574
|
+
run_watch_loop(
|
|
575
|
+
watcher, max_watch_iterations, term_lines_rewritable=term_lines_rewritable
|
|
576
|
+
)
|
|
577
|
+
except KeyboardInterrupt:
|
|
578
|
+
pass
|
|
579
|
+
watcher._pool.terminate()
|
|
580
|
+
print()
|
|
581
|
+
if random.uniform(0.0, 1.0) > 0.4:
|
|
582
|
+
motd = "I enjoyed working with you today!"
|
|
583
|
+
else:
|
|
584
|
+
motd = random.choice(_MOTD)
|
|
585
|
+
print(motd)
|
|
586
|
+
return 0
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def format_src_context(filename: str, lineno: int) -> str:
|
|
590
|
+
amount = 3
|
|
591
|
+
line_numbers = range(max(1, lineno - amount), lineno + amount + 1)
|
|
592
|
+
output = [f"{filename}:{lineno}:\n"]
|
|
593
|
+
for curline in line_numbers:
|
|
594
|
+
text = linecache.getline(filename, curline)
|
|
595
|
+
if text == "": # (actual empty lines have a newline)
|
|
596
|
+
continue
|
|
597
|
+
output.append(
|
|
598
|
+
">" + color(text, AnsiColor.WARNING) if lineno == curline else "|" + text
|
|
599
|
+
)
|
|
600
|
+
return "".join(output)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def describe_message(
|
|
604
|
+
message: AnalysisMessage, options: AnalysisOptions
|
|
605
|
+
) -> Optional[str]:
|
|
606
|
+
if options.report_verbose:
|
|
607
|
+
return long_describe_message(message, options)
|
|
608
|
+
else:
|
|
609
|
+
return short_describe_message(message, options)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def long_describe_message(
|
|
613
|
+
message: AnalysisMessage, options: AnalysisOptions
|
|
614
|
+
) -> Optional[str]:
|
|
615
|
+
tb, desc, state = message.traceback, message.message, message.state
|
|
616
|
+
desc = desc.replace(" when ", "\nwhen ")
|
|
617
|
+
context = format_src_context(message.filename, message.line)
|
|
618
|
+
intro = ""
|
|
619
|
+
if not options.report_all:
|
|
620
|
+
if message.state <= MessageType.PRE_UNSAT: # type: ignore
|
|
621
|
+
return None
|
|
622
|
+
if state == MessageType.CONFIRMED:
|
|
623
|
+
intro = "I was able to confirm your postcondition over all paths."
|
|
624
|
+
elif state == MessageType.CANNOT_CONFIRM:
|
|
625
|
+
intro = "I wasn't able to find a counterexample."
|
|
626
|
+
elif message.state == MessageType.PRE_UNSAT:
|
|
627
|
+
intro = "I am having trouble finding any inputs that meet your preconditions."
|
|
628
|
+
elif message.state == MessageType.POST_ERR:
|
|
629
|
+
intro = "I got an error while checking your postcondition."
|
|
630
|
+
elif message.state == MessageType.EXEC_ERR:
|
|
631
|
+
intro = "I found an exception while running your function."
|
|
632
|
+
elif message.state == MessageType.POST_FAIL:
|
|
633
|
+
intro = "I was able to make your postcondition return False."
|
|
634
|
+
elif message.state == MessageType.SYNTAX_ERR:
|
|
635
|
+
intro = "One of your conditions isn't a valid python expression."
|
|
636
|
+
elif message.state == MessageType.IMPORT_ERR:
|
|
637
|
+
intro = "I couldn't import a file."
|
|
638
|
+
if message.state <= MessageType.CANNOT_CONFIRM: # type: ignore
|
|
639
|
+
intro = color(intro, AnsiColor.OKGREEN)
|
|
640
|
+
else:
|
|
641
|
+
intro = color(intro, AnsiColor.FAIL)
|
|
642
|
+
return f"{tb}\n{intro}\n{context}\n{desc}\n"
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def short_describe_message(
|
|
646
|
+
message: AnalysisMessage, options: AnalysisOptions
|
|
647
|
+
) -> Optional[str]:
|
|
648
|
+
desc = message.message
|
|
649
|
+
if message.state <= MessageType.PRE_UNSAT: # type: ignore
|
|
650
|
+
if options.report_all:
|
|
651
|
+
return "{}:{}: {}: {}".format(message.filename, message.line, "info", desc)
|
|
652
|
+
return None
|
|
653
|
+
if message.state == MessageType.POST_ERR:
|
|
654
|
+
desc = "Error while evaluating post condition: " + desc
|
|
655
|
+
return "{}:{}: {}: {}".format(message.filename, message.line, "error", desc)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def checked_fn_load(qualname: str, stderr: TextIO) -> Optional[FunctionInfo]:
|
|
659
|
+
try:
|
|
660
|
+
objs = list(load_files_or_qualnames([qualname]))
|
|
661
|
+
except ErrorDuringImport as exc:
|
|
662
|
+
cause = exc.__cause__ if exc.__cause__ is not None else exc
|
|
663
|
+
print(
|
|
664
|
+
f'Unable to load "{qualname}": {type(cause).__name__}: {cause}',
|
|
665
|
+
file=stderr,
|
|
666
|
+
)
|
|
667
|
+
return None
|
|
668
|
+
obj = objs[0]
|
|
669
|
+
if not isinstance(obj, FunctionInfo):
|
|
670
|
+
print(f'"{qualname}" does not target a function.', file=stderr)
|
|
671
|
+
return None
|
|
672
|
+
if obj.get_callable() is None:
|
|
673
|
+
print(f'Cannot determine signature of "{qualname}"', file=stderr)
|
|
674
|
+
return None
|
|
675
|
+
return obj
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def checked_load(
|
|
679
|
+
target: str, stderr: TextIO
|
|
680
|
+
) -> Union[int, Iterable[Union[ModuleType, type, FunctionInfo]]]:
|
|
681
|
+
try:
|
|
682
|
+
return list(load_files_or_qualnames(target))
|
|
683
|
+
except FileNotFoundError as exc:
|
|
684
|
+
print(f'File not found: "{exc.args[0]}"', file=stderr)
|
|
685
|
+
return 2
|
|
686
|
+
except ErrorDuringImport as exc:
|
|
687
|
+
cause = exc.__cause__ if exc.__cause__ is not None else exc
|
|
688
|
+
print(f"Could not import your code:\n", file=stderr)
|
|
689
|
+
traceback.print_exception(type(cause), cause, cause.__traceback__, file=stderr)
|
|
690
|
+
return 2
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def diffbehavior(
|
|
694
|
+
args: argparse.Namespace, options: AnalysisOptions, stdout: TextIO, stderr: TextIO
|
|
695
|
+
) -> int:
|
|
696
|
+
(fn_name1, fn_name2) = (args.fn1, args.fn2)
|
|
697
|
+
fn1 = checked_fn_load(fn_name1, stderr)
|
|
698
|
+
fn2 = checked_fn_load(fn_name2, stderr)
|
|
699
|
+
exception_equivalence = args.exception_equivalence
|
|
700
|
+
if fn1 is None or fn2 is None:
|
|
701
|
+
return 2
|
|
702
|
+
options.stats = Counter()
|
|
703
|
+
diffs = diff_behavior(fn1, fn2, options, exception_equivalence)
|
|
704
|
+
debug("stats", options.stats)
|
|
705
|
+
if isinstance(diffs, str):
|
|
706
|
+
print(diffs, file=stderr)
|
|
707
|
+
return 2
|
|
708
|
+
elif len(diffs) == 0:
|
|
709
|
+
num_paths = options.stats["num_paths"]
|
|
710
|
+
exhausted = options.stats["exhaustion"] > 0
|
|
711
|
+
stdout.write(f"No differences found. (attempted {num_paths} iterations)\n")
|
|
712
|
+
if exhausted:
|
|
713
|
+
stdout.write("All paths exhausted, functions are likely the same!\n")
|
|
714
|
+
else:
|
|
715
|
+
stdout.write(
|
|
716
|
+
"Consider increasing the --max_uninteresting_iterations option.\n"
|
|
717
|
+
)
|
|
718
|
+
return 0
|
|
719
|
+
else:
|
|
720
|
+
width = max(len(fn_name1), len(fn_name2)) + 2
|
|
721
|
+
for diff in diffs:
|
|
722
|
+
inputs = ", ".join(f"{k}={v}" for k, v in diff.args.items())
|
|
723
|
+
stdout.write(f"Given: ({inputs}),\n")
|
|
724
|
+
result1, result2 = diff.result1, diff.result2
|
|
725
|
+
differing_args = result1.get_differing_arg_mutations(result2)
|
|
726
|
+
stdout.write(
|
|
727
|
+
f"{fn_name1.rjust(width)} : {result1.describe(differing_args)}\n"
|
|
728
|
+
)
|
|
729
|
+
stdout.write(
|
|
730
|
+
f"{fn_name2.rjust(width)} : {result2.describe(differing_args)}\n"
|
|
731
|
+
)
|
|
732
|
+
return 1
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def cover(
|
|
736
|
+
args: argparse.Namespace, options: AnalysisOptions, stdout: TextIO, stderr: TextIO
|
|
737
|
+
) -> int:
|
|
738
|
+
entities = checked_load(args.target, stderr)
|
|
739
|
+
if isinstance(entities, int):
|
|
740
|
+
return entities
|
|
741
|
+
to_be_processed = deque(entities)
|
|
742
|
+
fns = []
|
|
743
|
+
while to_be_processed:
|
|
744
|
+
entity = to_be_processed.pop()
|
|
745
|
+
if isinstance(entity, ModuleType):
|
|
746
|
+
to_be_processed.extend(
|
|
747
|
+
v for k, v in get_top_level_classes_and_functions(entity)
|
|
748
|
+
)
|
|
749
|
+
elif isinstance(entity, FunctionInfo):
|
|
750
|
+
fns.append(entity)
|
|
751
|
+
else:
|
|
752
|
+
assert isinstance(entity, type)
|
|
753
|
+
fns.extend(
|
|
754
|
+
FunctionInfo.from_class(entity, n)
|
|
755
|
+
for n, e in entity.__dict__.items()
|
|
756
|
+
if isinstance(e, FUNCTIONINFO_DESCRIPTOR_TYPES)
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
if not fns:
|
|
760
|
+
print("No functions or methods found.", file=stderr)
|
|
761
|
+
return 2
|
|
762
|
+
example_output_format = args.example_output_format
|
|
763
|
+
options.stats = Counter()
|
|
764
|
+
imports, lines = set(), []
|
|
765
|
+
for ctxfn in fns:
|
|
766
|
+
debug("Begin cover on", ctxfn.name)
|
|
767
|
+
pair = ctxfn.get_callable()
|
|
768
|
+
if pair is None:
|
|
769
|
+
continue
|
|
770
|
+
fn = pair[0]
|
|
771
|
+
|
|
772
|
+
try:
|
|
773
|
+
paths = path_cover(
|
|
774
|
+
ctxfn,
|
|
775
|
+
options,
|
|
776
|
+
args.coverage_type,
|
|
777
|
+
arg_formatter=(
|
|
778
|
+
format_boundargs_as_dictionary
|
|
779
|
+
if example_output_format == ExampleOutputFormat.ARG_DICTIONARY
|
|
780
|
+
else format_boundargs
|
|
781
|
+
),
|
|
782
|
+
)
|
|
783
|
+
except NotDeterministic:
|
|
784
|
+
print(
|
|
785
|
+
"Repeated executions are not behaving deterministically.", file=stderr
|
|
786
|
+
)
|
|
787
|
+
if not in_debug():
|
|
788
|
+
print("Re-run in verbose mode for debugging information.", file=stderr)
|
|
789
|
+
return 2
|
|
790
|
+
if example_output_format == ExampleOutputFormat.ARG_DICTIONARY:
|
|
791
|
+
output_argument_dictionary_paths(fn, paths, stdout, stderr)
|
|
792
|
+
elif example_output_format == ExampleOutputFormat.EVAL_EXPRESSION:
|
|
793
|
+
output_eval_exression_paths(fn, paths, stdout, stderr)
|
|
794
|
+
elif example_output_format == ExampleOutputFormat.PYTEST:
|
|
795
|
+
(cur_imports, cur_lines) = output_pytest_paths(fn, paths)
|
|
796
|
+
imports |= cur_imports
|
|
797
|
+
# imports.add(f"import {fn.__qualname__}")
|
|
798
|
+
lines.extend(cur_lines)
|
|
799
|
+
else:
|
|
800
|
+
assert False, "unexpected output format"
|
|
801
|
+
if example_output_format == ExampleOutputFormat.PYTEST:
|
|
802
|
+
stdout.write("\n".join(sorted(imports) + [""] + lines) + "\n")
|
|
803
|
+
stdout.flush()
|
|
804
|
+
|
|
805
|
+
return 0
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def search(
|
|
809
|
+
args: argparse.Namespace, options: AnalysisOptions, stdout: TextIO, stderr: TextIO
|
|
810
|
+
):
|
|
811
|
+
ctxfn = checked_fn_load(args.fn, stderr)
|
|
812
|
+
if ctxfn is None:
|
|
813
|
+
return 2
|
|
814
|
+
fn, _ = ctxfn.callable()
|
|
815
|
+
|
|
816
|
+
score: Optional[Callable] = None
|
|
817
|
+
optimization_kind: OptimizationKind = args.optimization
|
|
818
|
+
output_all_examples: bool = args.output_all_examples
|
|
819
|
+
|
|
820
|
+
argument_formatter = args.argument_formatter
|
|
821
|
+
if argument_formatter:
|
|
822
|
+
argument_formatter = checked_fn_load(argument_formatter, stderr)
|
|
823
|
+
if argument_formatter is None:
|
|
824
|
+
return 2
|
|
825
|
+
else:
|
|
826
|
+
argument_formatter, _ = argument_formatter.callable()
|
|
827
|
+
|
|
828
|
+
final_example: Optional[str] = None
|
|
829
|
+
|
|
830
|
+
def on_example(example: str) -> None:
|
|
831
|
+
if output_all_examples:
|
|
832
|
+
stdout.write(example + "\n")
|
|
833
|
+
nonlocal final_example
|
|
834
|
+
final_example = example
|
|
835
|
+
|
|
836
|
+
path_search(
|
|
837
|
+
ctxfn, options, argument_formatter, optimization_kind, score, on_example
|
|
838
|
+
)
|
|
839
|
+
if final_example is None:
|
|
840
|
+
stderr.write("No input found.\n")
|
|
841
|
+
stderr.write("Consider increasing the --max_uninteresting_iterations option.\n")
|
|
842
|
+
return 1
|
|
843
|
+
else:
|
|
844
|
+
if not output_all_examples:
|
|
845
|
+
stdout.write(final_example + "\n")
|
|
846
|
+
return 0
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def server(
|
|
850
|
+
args: argparse.Namespace, options: AnalysisOptionSet, stdout: TextIO, stderr: TextIO
|
|
851
|
+
) -> NoReturn:
|
|
852
|
+
from crosshair.lsp_server import create_lsp_server # (defer import for performance)
|
|
853
|
+
|
|
854
|
+
cast(Callable[[], NoReturn], create_lsp_server(options).start_io)()
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def check(
|
|
858
|
+
args: argparse.Namespace, options: AnalysisOptionSet, stdout: TextIO, stderr: TextIO
|
|
859
|
+
) -> int:
|
|
860
|
+
any_problems = False
|
|
861
|
+
entities = checked_load(args.target, stderr)
|
|
862
|
+
if isinstance(entities, int):
|
|
863
|
+
return entities
|
|
864
|
+
full_options = DEFAULT_OPTIONS.overlay(report_verbose=False).overlay(options)
|
|
865
|
+
checkables = [c for e in entities for c in analyze_any(e, options)]
|
|
866
|
+
if not checkables:
|
|
867
|
+
extra_help = ""
|
|
868
|
+
if full_options.analysis_kind == [AnalysisKind.asserts]:
|
|
869
|
+
extra_help = "\nHINT: Ensure that your functions to analyze lead with assert statements."
|
|
870
|
+
print(
|
|
871
|
+
"WARNING: Targets found, but contain no checkable functions." + extra_help,
|
|
872
|
+
file=stderr,
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
for message in run_checkables(checkables):
|
|
876
|
+
line = describe_message(message, full_options)
|
|
877
|
+
if line is None:
|
|
878
|
+
continue
|
|
879
|
+
stdout.write(line + "\n")
|
|
880
|
+
debug("Traceback for output message:\n", message.traceback)
|
|
881
|
+
if message.state > MessageType.PRE_UNSAT:
|
|
882
|
+
any_problems = True
|
|
883
|
+
return 1 if any_problems else 0
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def unwalled_main(cmd_args: Union[List[str], argparse.Namespace]) -> int:
|
|
887
|
+
parser = command_line_parser()
|
|
888
|
+
if isinstance(cmd_args, argparse.Namespace):
|
|
889
|
+
args = cmd_args
|
|
890
|
+
else:
|
|
891
|
+
args = parser.parse_args(cmd_args)
|
|
892
|
+
if not args.action:
|
|
893
|
+
parser.print_help(sys.stderr)
|
|
894
|
+
return 2
|
|
895
|
+
set_debug(args.verbose)
|
|
896
|
+
if in_debug():
|
|
897
|
+
debug(env_info())
|
|
898
|
+
debug("Installed plugins:", installed_plugins)
|
|
899
|
+
options = option_set_from_dict(args.__dict__)
|
|
900
|
+
# fall back to current directory to look up modules
|
|
901
|
+
path_additions = [""] if sys.path and sys.path[0] != "" else []
|
|
902
|
+
with add_to_pypath(*path_additions), prefer_pure_python_imports():
|
|
903
|
+
if args.extra_plugin:
|
|
904
|
+
for plugin in args.extra_plugin:
|
|
905
|
+
exec(Path(plugin).read_text())
|
|
906
|
+
if len(REGISTERED_CONTRACTS):
|
|
907
|
+
debug(
|
|
908
|
+
f"Registered {len(REGISTERED_CONTRACTS)} contract(s) "
|
|
909
|
+
f"from: {args.extra_plugin}"
|
|
910
|
+
)
|
|
911
|
+
if args.action == "check":
|
|
912
|
+
return check(args, options, sys.stdout, sys.stderr)
|
|
913
|
+
elif args.action == "search":
|
|
914
|
+
return search(
|
|
915
|
+
args, DEFAULT_OPTIONS.overlay(options), sys.stdout, sys.stderr
|
|
916
|
+
)
|
|
917
|
+
elif args.action == "diffbehavior":
|
|
918
|
+
defaults = DEFAULT_OPTIONS.overlay(
|
|
919
|
+
AnalysisOptionSet(
|
|
920
|
+
per_path_timeout=30.0, # mostly, we don't want to time out paths
|
|
921
|
+
)
|
|
922
|
+
)
|
|
923
|
+
return diffbehavior(args, defaults.overlay(options), sys.stdout, sys.stderr)
|
|
924
|
+
elif args.action == "cover":
|
|
925
|
+
defaults = DEFAULT_OPTIONS.overlay(
|
|
926
|
+
AnalysisOptionSet(
|
|
927
|
+
per_path_timeout=30.0, # mostly, we don't want to time out paths
|
|
928
|
+
)
|
|
929
|
+
)
|
|
930
|
+
return cover(args, defaults.overlay(options), sys.stdout, sys.stderr)
|
|
931
|
+
elif args.action == "watch":
|
|
932
|
+
disable_auditwall() # (we'll engage auditwall in the workers)
|
|
933
|
+
return watch(args, options)
|
|
934
|
+
elif args.action == "server":
|
|
935
|
+
disable_auditwall() # (we'll engage auditwall in the workers)
|
|
936
|
+
server(args, options, sys.stdout, sys.stderr)
|
|
937
|
+
else:
|
|
938
|
+
print(f'Unknown action: "{args.action}"', file=sys.stderr)
|
|
939
|
+
return 2
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def mypy_and_check(cmd_args: Optional[List[str]] = None) -> None:
|
|
943
|
+
if cmd_args is None:
|
|
944
|
+
cmd_args = sys.argv[1:]
|
|
945
|
+
cmd_args = ["check"] + cmd_args
|
|
946
|
+
check_args, mypy_args = command_line_parser().parse_known_args(cmd_args)
|
|
947
|
+
set_debug(check_args.verbose)
|
|
948
|
+
mypy_cmd_args = mypy_args + check_args.target
|
|
949
|
+
debug("Running mypy with the following arguments:", " ".join(mypy_cmd_args))
|
|
950
|
+
try:
|
|
951
|
+
from mypy import api
|
|
952
|
+
except ModuleNotFoundError:
|
|
953
|
+
print("Unable to find mypy; skipping", file=sys.stderr)
|
|
954
|
+
else:
|
|
955
|
+
_mypy_out, mypy_err, mypy_ret = api.run(mypy_cmd_args)
|
|
956
|
+
print(mypy_err, file=sys.stderr)
|
|
957
|
+
if mypy_ret != 0:
|
|
958
|
+
print(_mypy_out, file=sys.stdout)
|
|
959
|
+
sys.exit(mypy_ret)
|
|
960
|
+
engage_auditwall()
|
|
961
|
+
debug("Running crosshair with these args:", check_args)
|
|
962
|
+
sys.exit(unwalled_main(check_args))
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def main(cmd_args: Optional[List[str]] = None) -> None:
|
|
966
|
+
if cmd_args is None:
|
|
967
|
+
cmd_args = sys.argv[1:]
|
|
968
|
+
engage_auditwall()
|
|
969
|
+
sys.exit(unwalled_main(cmd_args))
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
if __name__ == "__main__":
|
|
973
|
+
main()
|