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/util_test.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import traceback
|
|
3
|
+
import types
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from inspect import signature
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
import numpy
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from crosshair.tracers import PatchingModule
|
|
13
|
+
from crosshair.util import (
|
|
14
|
+
CrossHairInternal,
|
|
15
|
+
DynamicScopeVar,
|
|
16
|
+
eval_friendly_repr,
|
|
17
|
+
format_boundargs,
|
|
18
|
+
imported_alternative,
|
|
19
|
+
is_pure_python,
|
|
20
|
+
renamed_function,
|
|
21
|
+
sourcelines,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_is_pure_python_functions():
|
|
26
|
+
assert is_pure_python(is_pure_python)
|
|
27
|
+
assert not is_pure_python(map)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_is_pure_python_classes():
|
|
31
|
+
class RegularClass:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
class ClassWithSlots:
|
|
35
|
+
__slots__ = ("x",)
|
|
36
|
+
|
|
37
|
+
assert is_pure_python(RegularClass)
|
|
38
|
+
assert is_pure_python(ClassWithSlots)
|
|
39
|
+
assert not is_pure_python(list)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_is_pure_python_other_stuff():
|
|
43
|
+
assert is_pure_python(7)
|
|
44
|
+
assert is_pure_python(pytest)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_dynamic_scope_var_basic():
|
|
48
|
+
var = DynamicScopeVar(int, "height")
|
|
49
|
+
with var.open(7):
|
|
50
|
+
assert var.get() == 7
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_dynamic_scope_var_bsic():
|
|
54
|
+
var = DynamicScopeVar(int, "height")
|
|
55
|
+
assert var.get_if_in_scope() is None
|
|
56
|
+
with var.open(7):
|
|
57
|
+
assert var.get_if_in_scope() == 7
|
|
58
|
+
assert var.get_if_in_scope() is None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_dynamic_scope_var_error_cases():
|
|
62
|
+
var = DynamicScopeVar(int, "height")
|
|
63
|
+
with var.open(100):
|
|
64
|
+
with pytest.raises(AssertionError, match="Already in a height context"):
|
|
65
|
+
with var.open(500, reentrant=False):
|
|
66
|
+
pass
|
|
67
|
+
with pytest.raises(AssertionError, match="Not in a height context"):
|
|
68
|
+
var.get()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_dynamic_scope_var_with_exception():
|
|
72
|
+
var = DynamicScopeVar(int, "height")
|
|
73
|
+
try:
|
|
74
|
+
with var.open(7):
|
|
75
|
+
raise NameError()
|
|
76
|
+
except NameError:
|
|
77
|
+
pass
|
|
78
|
+
assert var.get_if_in_scope() is None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_imported_alternative():
|
|
82
|
+
import heapq
|
|
83
|
+
|
|
84
|
+
assert type(heapq.heapify) == types.BuiltinFunctionType
|
|
85
|
+
with imported_alternative("heapq", ("_heapq",)):
|
|
86
|
+
assert type(heapq.heapify) == types.FunctionType
|
|
87
|
+
assert type(heapq.heapify) == types.BuiltinFunctionType
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class UnhashableCallable:
|
|
91
|
+
def __hash__(self):
|
|
92
|
+
raise CrossHairInternal("Do not hash")
|
|
93
|
+
|
|
94
|
+
def __call__(self):
|
|
95
|
+
return 42
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_sourcelines_on_unhashable_callable():
|
|
99
|
+
# Ensure we never trigger hashing when getting source code.
|
|
100
|
+
sourcelines(UnhashableCallable())
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def eat_things(p1, *varp, kw1=4, kw2="default", **varkw):
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_format_boundargs():
|
|
108
|
+
bound = signature(eat_things).bind(1, 2, 3, kw2=5, other=6)
|
|
109
|
+
assert format_boundargs(bound) == "1, 2, 3, kw1=4, kw2=5, other=6"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Color(Enum):
|
|
113
|
+
RED = 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class Pair:
|
|
118
|
+
x: Union["Pair", type, None] = None
|
|
119
|
+
y: Union["Pair", type, None] = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_eval_friendly_repr():
|
|
123
|
+
from crosshair.opcode_intercept import FormatValueInterceptor
|
|
124
|
+
from crosshair.tracers import COMPOSITE_TRACER, NoTracing, PushedModule
|
|
125
|
+
|
|
126
|
+
with COMPOSITE_TRACER, PushedModule(PatchingModule()), PushedModule(
|
|
127
|
+
FormatValueInterceptor()
|
|
128
|
+
), NoTracing():
|
|
129
|
+
# Class
|
|
130
|
+
assert eval_friendly_repr(Color) == "Color"
|
|
131
|
+
# Pure-python method:
|
|
132
|
+
assert (
|
|
133
|
+
eval_friendly_repr(UnhashableCallable.__hash__)
|
|
134
|
+
== "UnhashableCallable.__hash__"
|
|
135
|
+
)
|
|
136
|
+
# Builtin function:
|
|
137
|
+
assert eval_friendly_repr(print) == "print"
|
|
138
|
+
# Object:
|
|
139
|
+
assert eval_friendly_repr(object()) == "object()"
|
|
140
|
+
# Special float values:
|
|
141
|
+
assert eval_friendly_repr(float("nan")) == 'float("nan")'
|
|
142
|
+
# MethodDescriptorType
|
|
143
|
+
assert isinstance(str.join, types.MethodDescriptorType)
|
|
144
|
+
assert eval_friendly_repr(str.join) == "str.join"
|
|
145
|
+
# enums:
|
|
146
|
+
assert eval_friendly_repr(Color.RED) == "Color.RED"
|
|
147
|
+
# basic dataclass
|
|
148
|
+
assert eval_friendly_repr(Pair(None, None)) == "Pair(x=None, y=None)"
|
|
149
|
+
# do not attempt to re-use ReferencedIdentifiers
|
|
150
|
+
assert eval_friendly_repr(Pair(Pair, Pair)) == "Pair(x=Pair, y=Pair)"
|
|
151
|
+
# Preserve identical objects
|
|
152
|
+
a = Pair()
|
|
153
|
+
assert (
|
|
154
|
+
eval_friendly_repr(Pair(a, a)) == "Pair(x=v1:=Pair(x=None, y=None), y=v1)"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# We return to original repr() behaviors afterwards:
|
|
158
|
+
assert repr(float("nan")) == "nan"
|
|
159
|
+
assert repr(Color.RED) == "<Color.RED: 0>"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_renamed_function():
|
|
163
|
+
def crash_on_seven(x):
|
|
164
|
+
if x == 7:
|
|
165
|
+
raise IOError
|
|
166
|
+
return x
|
|
167
|
+
|
|
168
|
+
hello = renamed_function(crash_on_seven, "hello")
|
|
169
|
+
hello(6)
|
|
170
|
+
try:
|
|
171
|
+
hello(7)
|
|
172
|
+
except IOError as e:
|
|
173
|
+
assert traceback.extract_tb(e.__traceback__)[-1].name == "hello"
|
crosshair/watcher.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import binascii
|
|
3
|
+
import multiprocessing
|
|
4
|
+
import os
|
|
5
|
+
import pickle
|
|
6
|
+
import queue
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
import traceback
|
|
12
|
+
import zlib
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from queue import Queue
|
|
15
|
+
from typing import (
|
|
16
|
+
Any,
|
|
17
|
+
Counter,
|
|
18
|
+
Dict,
|
|
19
|
+
Iterable,
|
|
20
|
+
Iterator,
|
|
21
|
+
List,
|
|
22
|
+
Optional,
|
|
23
|
+
Set,
|
|
24
|
+
Tuple,
|
|
25
|
+
Union,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from crosshair.auditwall import engage_auditwall, opened_auditwall
|
|
29
|
+
from crosshair.core_and_libs import (
|
|
30
|
+
AnalysisMessage,
|
|
31
|
+
MessageType,
|
|
32
|
+
analyze_module,
|
|
33
|
+
run_checkables,
|
|
34
|
+
)
|
|
35
|
+
from crosshair.fnutil import NotFound, walk_paths
|
|
36
|
+
from crosshair.options import AnalysisOptionSet
|
|
37
|
+
from crosshair.util import (
|
|
38
|
+
CrossHairInternal,
|
|
39
|
+
ErrorDuringImport,
|
|
40
|
+
debug,
|
|
41
|
+
load_file,
|
|
42
|
+
set_debug,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Use "spawn" in stead of fork() because we've already imported the code we're watching:
|
|
46
|
+
multiproc_spawn = multiprocessing.get_context("spawn")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def mtime(path: Path) -> Optional[float]:
|
|
50
|
+
try:
|
|
51
|
+
return path.stat().st_mtime
|
|
52
|
+
except FileNotFoundError:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
WorkItemInput = Tuple[Path, AnalysisOptionSet, float] # (file, opts, deadline)
|
|
57
|
+
WorkItemOutput = Tuple[Path, Counter[str], List[AnalysisMessage]]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def serialize(obj: object) -> str:
|
|
61
|
+
return str(base64.b64encode(zlib.compress(pickle.dumps(obj))), "ascii")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def deserialize(data: Union[bytes, str]) -> Any:
|
|
65
|
+
try:
|
|
66
|
+
return pickle.loads(zlib.decompress(base64.b64decode(data)))
|
|
67
|
+
except binascii.Error:
|
|
68
|
+
debug(f"Unable to deserialize this data: {data!r}")
|
|
69
|
+
raise
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def import_error_msg(err: ErrorDuringImport) -> AnalysisMessage:
|
|
73
|
+
cause = err.__cause__ if err.__cause__ else err
|
|
74
|
+
tb = cause.__traceback__
|
|
75
|
+
if tb:
|
|
76
|
+
filename, line = tb.tb_frame.f_code.co_filename, tb.tb_lineno
|
|
77
|
+
tbstr = "".join(traceback.format_tb(tb))
|
|
78
|
+
else:
|
|
79
|
+
filename, line = "<unknown>", 0
|
|
80
|
+
tbstr = ""
|
|
81
|
+
return AnalysisMessage(MessageType.IMPORT_ERR, str(cause), filename, line, 0, tbstr)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def pool_worker_process_item(
|
|
85
|
+
item: WorkItemInput,
|
|
86
|
+
) -> Tuple[Counter[str], List[AnalysisMessage]]:
|
|
87
|
+
filename, options, deadline = item
|
|
88
|
+
stats: Counter[str] = Counter()
|
|
89
|
+
options.stats = stats
|
|
90
|
+
try:
|
|
91
|
+
module = load_file(str(filename))
|
|
92
|
+
except NotFound as e:
|
|
93
|
+
debug(f'Not analyzing "{filename}" because sub-module import failed: {e}')
|
|
94
|
+
return (stats, [])
|
|
95
|
+
except ErrorDuringImport as e:
|
|
96
|
+
debug(f'Not analyzing "{filename}" because import failed: {e}')
|
|
97
|
+
return (stats, [import_error_msg(e)])
|
|
98
|
+
messages = run_checkables(analyze_module(module, options))
|
|
99
|
+
return (stats, messages)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class PoolWorkerShell(threading.Thread):
|
|
103
|
+
def __init__(
|
|
104
|
+
self, input_item: WorkItemInput, results: "queue.Queue[WorkItemOutput]"
|
|
105
|
+
):
|
|
106
|
+
self.input_item = input_item
|
|
107
|
+
self.results = results
|
|
108
|
+
self.proc: Optional[subprocess.Popen] = None
|
|
109
|
+
super().__init__()
|
|
110
|
+
|
|
111
|
+
def run(self) -> None:
|
|
112
|
+
encoded_input = serialize(self.input_item)
|
|
113
|
+
worker_args = [
|
|
114
|
+
sys.executable,
|
|
115
|
+
"-c",
|
|
116
|
+
f"import crosshair.watcher; crosshair.watcher.pool_worker_main()",
|
|
117
|
+
encoded_input,
|
|
118
|
+
]
|
|
119
|
+
self.proc = subprocess.Popen(
|
|
120
|
+
worker_args,
|
|
121
|
+
stdin=subprocess.DEVNULL,
|
|
122
|
+
stdout=subprocess.PIPE,
|
|
123
|
+
stderr=subprocess.PIPE,
|
|
124
|
+
)
|
|
125
|
+
(stdout, _stderr) = self.proc.communicate(b"")
|
|
126
|
+
if stdout:
|
|
127
|
+
last_line = stdout.splitlines()[-1] # (in case of spurious print()s)
|
|
128
|
+
self.results.put(deserialize(last_line))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def pool_worker_main() -> None:
|
|
132
|
+
item: WorkItemInput = deserialize(sys.argv[-1])
|
|
133
|
+
filename = item[0]
|
|
134
|
+
try:
|
|
135
|
+
if hasattr(os, "nice"): # analysis should run at a low priority
|
|
136
|
+
# Note that the following "type: ignore" is ONLY required for mypy on
|
|
137
|
+
# Windows, where the nice function does not exist:
|
|
138
|
+
os.nice(10) # type: ignore
|
|
139
|
+
set_debug(False)
|
|
140
|
+
engage_auditwall()
|
|
141
|
+
(stats, messages) = pool_worker_process_item(item)
|
|
142
|
+
output: WorkItemOutput = (filename, stats, messages)
|
|
143
|
+
print(serialize(output))
|
|
144
|
+
except BaseException as e:
|
|
145
|
+
raise CrossHairInternal("Worker failed while analyzing " + str(filename)) from e
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class Pool:
|
|
149
|
+
_workers: List[Tuple[PoolWorkerShell, WorkItemInput]]
|
|
150
|
+
_work: List[WorkItemInput]
|
|
151
|
+
_results: "Queue[WorkItemOutput]"
|
|
152
|
+
_max_processes: int
|
|
153
|
+
|
|
154
|
+
def __init__(self, max_processes: int) -> None:
|
|
155
|
+
self._workers = []
|
|
156
|
+
self._work = []
|
|
157
|
+
self._results = Queue()
|
|
158
|
+
self._max_processes = max_processes
|
|
159
|
+
|
|
160
|
+
def _spawn_workers(self):
|
|
161
|
+
work_list = self._work
|
|
162
|
+
workers = self._workers
|
|
163
|
+
while work_list and len(self._workers) < self._max_processes:
|
|
164
|
+
work_item = work_list.pop()
|
|
165
|
+
# NOTE: We are martialling data manually.
|
|
166
|
+
# Earlier versions used multiprocessing and Queues, but
|
|
167
|
+
# multiprocessing.Process is incompatible with pygls on windows
|
|
168
|
+
# (something with the async blocking on stdin, which must remain open
|
|
169
|
+
# in the child).
|
|
170
|
+
|
|
171
|
+
thread = PoolWorkerShell(work_item, self._results)
|
|
172
|
+
workers.append((thread, work_item))
|
|
173
|
+
thread.start()
|
|
174
|
+
|
|
175
|
+
def _prune_workers(self, curtime: float) -> None:
|
|
176
|
+
for worker, item in self._workers:
|
|
177
|
+
(_, _, deadline) = item
|
|
178
|
+
if worker.is_alive() and curtime > deadline and worker.proc is not None:
|
|
179
|
+
debug("Killing worker over deadline", worker)
|
|
180
|
+
worker.proc.terminate()
|
|
181
|
+
time.sleep(0.5)
|
|
182
|
+
if worker.is_alive():
|
|
183
|
+
worker.proc.kill()
|
|
184
|
+
worker.join()
|
|
185
|
+
self._workers = [(w, i) for w, i in self._workers if w.is_alive()]
|
|
186
|
+
|
|
187
|
+
def terminate(self) -> None:
|
|
188
|
+
self._prune_workers(float("+inf"))
|
|
189
|
+
self._work = []
|
|
190
|
+
|
|
191
|
+
def garden_workers(self) -> None:
|
|
192
|
+
self._prune_workers(time.time())
|
|
193
|
+
self._spawn_workers()
|
|
194
|
+
|
|
195
|
+
def is_working(self) -> bool:
|
|
196
|
+
return bool(self._workers or self._work)
|
|
197
|
+
|
|
198
|
+
def submit(self, item: WorkItemInput) -> None:
|
|
199
|
+
self._work.append(item)
|
|
200
|
+
|
|
201
|
+
def has_result(self) -> bool:
|
|
202
|
+
return not self._results.empty()
|
|
203
|
+
|
|
204
|
+
def get_result(self, timeout: float) -> Optional[WorkItemOutput]:
|
|
205
|
+
try:
|
|
206
|
+
return self._results.get(timeout=timeout)
|
|
207
|
+
except queue.Empty:
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class Watcher:
|
|
212
|
+
_paths: Set[Path]
|
|
213
|
+
_pool: Pool
|
|
214
|
+
_modtimes: Dict[Path, float]
|
|
215
|
+
_options: AnalysisOptionSet
|
|
216
|
+
_next_file_check: float = 0.0
|
|
217
|
+
_change_flag: bool = False
|
|
218
|
+
_stop_flag: bool = False
|
|
219
|
+
|
|
220
|
+
def __init__(
|
|
221
|
+
self, files: Iterable[Path], options: AnalysisOptionSet = AnalysisOptionSet()
|
|
222
|
+
):
|
|
223
|
+
self._paths = set(files)
|
|
224
|
+
self._pool = self.startpool()
|
|
225
|
+
self._modtimes = {}
|
|
226
|
+
self._options = options
|
|
227
|
+
|
|
228
|
+
def shutdown(self):
|
|
229
|
+
self._stop_flag = True
|
|
230
|
+
|
|
231
|
+
def update_paths(self, paths: Iterable[Path]):
|
|
232
|
+
self._paths = set(paths)
|
|
233
|
+
|
|
234
|
+
def startpool(self) -> Pool:
|
|
235
|
+
return Pool(multiprocessing.cpu_count() - 1)
|
|
236
|
+
|
|
237
|
+
def run_iteration(
|
|
238
|
+
self, max_uninteresting_iterations=5
|
|
239
|
+
) -> Iterator[Tuple[Counter[str], List[AnalysisMessage]]]:
|
|
240
|
+
debug(
|
|
241
|
+
f"starting pass with max_uninteresting_iterations={max_uninteresting_iterations}"
|
|
242
|
+
)
|
|
243
|
+
debug("Files:", self._modtimes.keys())
|
|
244
|
+
pool = self._pool
|
|
245
|
+
for filename, _ in sorted(self._modtimes.items(), key=lambda pair: -pair[1]):
|
|
246
|
+
worker_timeout = max(
|
|
247
|
+
10.0, max_uninteresting_iterations * 1_000.0
|
|
248
|
+
) # TODO: times 1000? is that right?
|
|
249
|
+
iter_options = AnalysisOptionSet(
|
|
250
|
+
max_uninteresting_iterations=max_uninteresting_iterations,
|
|
251
|
+
)
|
|
252
|
+
options = self._options.overlay(iter_options)
|
|
253
|
+
pool.submit((filename, options, time.time() + worker_timeout))
|
|
254
|
+
|
|
255
|
+
pool.garden_workers()
|
|
256
|
+
if not pool.is_working():
|
|
257
|
+
# Unusual case where there is nothing to do:
|
|
258
|
+
time.sleep(1.5)
|
|
259
|
+
self.handle_periodic() # (keep checking for changes!)
|
|
260
|
+
yield (Counter(), [])
|
|
261
|
+
return
|
|
262
|
+
while pool.is_working():
|
|
263
|
+
result = pool.get_result(timeout=1.0)
|
|
264
|
+
if result is not None:
|
|
265
|
+
(_, counters, messages) = result
|
|
266
|
+
yield (counters, messages)
|
|
267
|
+
if pool.has_result():
|
|
268
|
+
continue
|
|
269
|
+
if self.handle_periodic():
|
|
270
|
+
yield (Counter(), []) # to break the parent from waiting
|
|
271
|
+
return
|
|
272
|
+
pool.garden_workers()
|
|
273
|
+
debug("Worker pool tasks complete")
|
|
274
|
+
|
|
275
|
+
def handle_periodic(self) -> bool:
|
|
276
|
+
if self._stop_flag:
|
|
277
|
+
debug("Aborting iteration on shutdown request")
|
|
278
|
+
self._pool.terminate()
|
|
279
|
+
return True
|
|
280
|
+
if time.time() >= self._next_file_check:
|
|
281
|
+
self._next_file_check = time.time() + 1.0
|
|
282
|
+
if self.check_changed():
|
|
283
|
+
self._change_flag = True
|
|
284
|
+
debug("Aborting iteration on change detection")
|
|
285
|
+
self._pool.terminate()
|
|
286
|
+
self._pool = self.startpool()
|
|
287
|
+
return True
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
def check_changed(self) -> bool:
|
|
291
|
+
unchecked_modtimes = self._modtimes.copy()
|
|
292
|
+
changed = False
|
|
293
|
+
for curfile in walk_paths(self._paths, ignore_missing=True):
|
|
294
|
+
cur_mtime = mtime(curfile)
|
|
295
|
+
if cur_mtime is None:
|
|
296
|
+
# Unlikely; race condition on an interleaved file delete
|
|
297
|
+
continue
|
|
298
|
+
if cur_mtime == unchecked_modtimes.pop(curfile, None):
|
|
299
|
+
continue
|
|
300
|
+
changed = True
|
|
301
|
+
self._modtimes[curfile] = cur_mtime
|
|
302
|
+
if unchecked_modtimes:
|
|
303
|
+
# Files known but not found; something was deleted
|
|
304
|
+
changed = True
|
|
305
|
+
for delfile in unchecked_modtimes.keys():
|
|
306
|
+
del self._modtimes[delfile]
|
|
307
|
+
return changed
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from crosshair.statespace import MessageType
|
|
9
|
+
from crosshair.test_util import simplefs
|
|
10
|
+
from crosshair.watcher import Watcher
|
|
11
|
+
|
|
12
|
+
# TODO: DRY a bit with main_test.py
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(autouse=True)
|
|
16
|
+
def rewind_modules():
|
|
17
|
+
defined_modules = list(sys.modules.keys())
|
|
18
|
+
yield None
|
|
19
|
+
for name, module in list(sys.modules.items()):
|
|
20
|
+
# Some standard library modules aren't happy with getting reloaded.
|
|
21
|
+
if name.startswith("multiprocessing"):
|
|
22
|
+
continue
|
|
23
|
+
if name not in defined_modules:
|
|
24
|
+
del sys.modules[name]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
BUGGY_FOO = {
|
|
28
|
+
"foo.py": """
|
|
29
|
+
def foofn(x: int) -> int:
|
|
30
|
+
''' post: _ == x '''
|
|
31
|
+
print("this print does not confuse the watcher")
|
|
32
|
+
return x + 1
|
|
33
|
+
"""
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
CORRECT_FOO = {
|
|
37
|
+
"foo.py": """
|
|
38
|
+
def foofn(x: int) -> int:
|
|
39
|
+
''' post: _ == 1 + x '''
|
|
40
|
+
return x + 1
|
|
41
|
+
"""
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
BAD_SYNTAX_FOO = {
|
|
45
|
+
"foo.py": """
|
|
46
|
+
def foofn(x: int) -> int:
|
|
47
|
+
''' post: _ == x '''
|
|
48
|
+
return $ x + 1
|
|
49
|
+
"""
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
EMPTY_BAR = {
|
|
53
|
+
"bar.py": """
|
|
54
|
+
# Nothing here
|
|
55
|
+
"""
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_added_file(tmp_path: Path):
|
|
60
|
+
simplefs(tmp_path, CORRECT_FOO)
|
|
61
|
+
watcher = Watcher([tmp_path])
|
|
62
|
+
assert watcher.check_changed()
|
|
63
|
+
assert not watcher.check_changed()
|
|
64
|
+
simplefs(tmp_path, EMPTY_BAR)
|
|
65
|
+
assert watcher.check_changed()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_added_file_to_empty_dir(tmp_path: Path):
|
|
69
|
+
watcher = Watcher([tmp_path])
|
|
70
|
+
assert list(watcher.run_iteration()) == [(Counter(), [])]
|
|
71
|
+
simplefs(tmp_path, BUGGY_FOO) # Add new file.
|
|
72
|
+
watcher._next_file_check = time.time() - 1
|
|
73
|
+
# Detect file (yields empty result to wake up the loop)
|
|
74
|
+
assert list(watcher.run_iteration()) == [(Counter(), [])]
|
|
75
|
+
# Find bug in new file after restart.
|
|
76
|
+
results = list(watcher.run_iteration())
|
|
77
|
+
assert len(results) == 1
|
|
78
|
+
assert [m.state for m in results[0][1]] == [MessageType.POST_FAIL]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_modified_file(tmp_path: Path):
|
|
82
|
+
simplefs(tmp_path, CORRECT_FOO)
|
|
83
|
+
watcher = Watcher([tmp_path])
|
|
84
|
+
assert watcher.check_changed()
|
|
85
|
+
assert not watcher.check_changed()
|
|
86
|
+
time.sleep(0.01) # Ensure mtime is actually different!
|
|
87
|
+
simplefs(tmp_path, BUGGY_FOO)
|
|
88
|
+
assert watcher.check_changed()
|
|
89
|
+
assert not watcher.check_changed()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_removed_file(tmp_path: Path):
|
|
93
|
+
simplefs(tmp_path, CORRECT_FOO)
|
|
94
|
+
simplefs(tmp_path, EMPTY_BAR)
|
|
95
|
+
watcher = Watcher([tmp_path])
|
|
96
|
+
assert watcher.check_changed()
|
|
97
|
+
assert not watcher.check_changed()
|
|
98
|
+
(tmp_path / "bar.py").unlink()
|
|
99
|
+
assert watcher.check_changed()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_removed_file_given_as_argument(tmp_path: Path):
|
|
103
|
+
simplefs(tmp_path, CORRECT_FOO)
|
|
104
|
+
watcher = Watcher([tmp_path / "foo.py"])
|
|
105
|
+
assert watcher.check_changed()
|
|
106
|
+
(tmp_path / "foo.py").unlink()
|
|
107
|
+
assert watcher.check_changed()
|
crosshair/z3util.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import z3 # type: ignore
|
|
2
|
+
from z3 import (
|
|
3
|
+
BoolRef,
|
|
4
|
+
BoolSort,
|
|
5
|
+
ExprRef,
|
|
6
|
+
IntNumRef,
|
|
7
|
+
IntSort,
|
|
8
|
+
Z3_mk_and,
|
|
9
|
+
Z3_mk_eq,
|
|
10
|
+
Z3_mk_ge,
|
|
11
|
+
Z3_mk_gt,
|
|
12
|
+
Z3_mk_not,
|
|
13
|
+
Z3_mk_numeral,
|
|
14
|
+
Z3_mk_or,
|
|
15
|
+
Z3_solver_assert,
|
|
16
|
+
)
|
|
17
|
+
from z3.z3 import _to_ast_array # type: ignore
|
|
18
|
+
|
|
19
|
+
ctx = z3.main_ctx()
|
|
20
|
+
ctx_ref = ctx.ref()
|
|
21
|
+
bool_sort = BoolSort(ctx)
|
|
22
|
+
int_sort_ast = IntSort(ctx).ast
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def z3Eq(a: ExprRef, b: ExprRef) -> BoolRef:
|
|
26
|
+
# return a == b
|
|
27
|
+
return BoolRef(Z3_mk_eq(ctx_ref, a.as_ast(), b.as_ast()), ctx)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def z3Gt(a: IntNumRef, b: IntNumRef) -> BoolRef:
|
|
31
|
+
# return a > b
|
|
32
|
+
return BoolRef(Z3_mk_gt(ctx_ref, a.as_ast(), b.as_ast()), ctx)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def z3Ge(a: IntNumRef, b: IntNumRef) -> BoolRef:
|
|
36
|
+
# return a >= b
|
|
37
|
+
return BoolRef(Z3_mk_ge(ctx_ref, a.as_ast(), b.as_ast()), ctx)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def z3IntVal(x: int) -> z3.IntNumRef:
|
|
41
|
+
# return z3.IntVal(x)
|
|
42
|
+
# Use __index__ to get a regular integer for int subtypes (e.g. enums)
|
|
43
|
+
return IntNumRef(Z3_mk_numeral(ctx_ref, x.__index__().__str__(), int_sort_ast), ctx)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def z3Or(*exprs):
|
|
47
|
+
# return z3.Or(*exprs)
|
|
48
|
+
(args, sz) = _to_ast_array(exprs)
|
|
49
|
+
return BoolRef(Z3_mk_or(ctx.ref(), sz, args), ctx)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def z3And(*exprs):
|
|
53
|
+
# return z3.And(*exprs)
|
|
54
|
+
(args, sz) = _to_ast_array(exprs)
|
|
55
|
+
return BoolRef(Z3_mk_and(ctx.ref(), sz, args), ctx)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def z3Aassert(solver, expr):
|
|
59
|
+
# return solver.add(expr)
|
|
60
|
+
assert isinstance(expr, z3.ExprRef)
|
|
61
|
+
Z3_solver_assert(ctx_ref, solver.solver, expr.as_ast())
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def z3Not(expr):
|
|
65
|
+
# return z3.Not(expr)
|
|
66
|
+
if z3.is_not(expr):
|
|
67
|
+
return expr.arg(0)
|
|
68
|
+
else:
|
|
69
|
+
return BoolRef(Z3_mk_not(ctx_ref, expr.as_ast()), ctx)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def z3PopNot(expr):
|
|
73
|
+
if z3.is_not(expr):
|
|
74
|
+
return (False, expr.arg(0))
|
|
75
|
+
else:
|
|
76
|
+
return (True, expr)
|