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.
Files changed (176) hide show
  1. _crosshair_tracers.cpython-312-darwin.so +0 -0
  2. crosshair/__init__.py +42 -0
  3. crosshair/__main__.py +8 -0
  4. crosshair/_mark_stacks.h +790 -0
  5. crosshair/_preliminaries_test.py +18 -0
  6. crosshair/_tracers.h +94 -0
  7. crosshair/_tracers_pycompat.h +522 -0
  8. crosshair/_tracers_test.py +138 -0
  9. crosshair/abcstring.py +245 -0
  10. crosshair/auditwall.py +190 -0
  11. crosshair/auditwall_test.py +77 -0
  12. crosshair/codeconfig.py +113 -0
  13. crosshair/codeconfig_test.py +117 -0
  14. crosshair/condition_parser.py +1237 -0
  15. crosshair/condition_parser_test.py +497 -0
  16. crosshair/conftest.py +30 -0
  17. crosshair/copyext.py +155 -0
  18. crosshair/copyext_test.py +84 -0
  19. crosshair/core.py +1763 -0
  20. crosshair/core_and_libs.py +149 -0
  21. crosshair/core_regestered_types_test.py +82 -0
  22. crosshair/core_test.py +1316 -0
  23. crosshair/diff_behavior.py +314 -0
  24. crosshair/diff_behavior_test.py +261 -0
  25. crosshair/dynamic_typing.py +346 -0
  26. crosshair/dynamic_typing_test.py +210 -0
  27. crosshair/enforce.py +282 -0
  28. crosshair/enforce_test.py +182 -0
  29. crosshair/examples/PEP316/__init__.py +1 -0
  30. crosshair/examples/PEP316/bugs_detected/__init__.py +0 -0
  31. crosshair/examples/PEP316/bugs_detected/getattr_magic.py +16 -0
  32. crosshair/examples/PEP316/bugs_detected/hash_consistent_with_equals.py +31 -0
  33. crosshair/examples/PEP316/bugs_detected/shopping_cart.py +24 -0
  34. crosshair/examples/PEP316/bugs_detected/showcase.py +39 -0
  35. crosshair/examples/PEP316/correct_code/__init__.py +0 -0
  36. crosshair/examples/PEP316/correct_code/arith.py +60 -0
  37. crosshair/examples/PEP316/correct_code/chess.py +77 -0
  38. crosshair/examples/PEP316/correct_code/nesting_inference.py +17 -0
  39. crosshair/examples/PEP316/correct_code/numpy_examples.py +132 -0
  40. crosshair/examples/PEP316/correct_code/rolling_average.py +35 -0
  41. crosshair/examples/PEP316/correct_code/showcase.py +104 -0
  42. crosshair/examples/__init__.py +0 -0
  43. crosshair/examples/check_examples_test.py +146 -0
  44. crosshair/examples/deal/__init__.py +1 -0
  45. crosshair/examples/icontract/__init__.py +1 -0
  46. crosshair/examples/icontract/bugs_detected/__init__.py +0 -0
  47. crosshair/examples/icontract/bugs_detected/showcase.py +41 -0
  48. crosshair/examples/icontract/bugs_detected/wrong_sign.py +8 -0
  49. crosshair/examples/icontract/correct_code/__init__.py +0 -0
  50. crosshair/examples/icontract/correct_code/arith.py +51 -0
  51. crosshair/examples/icontract/correct_code/showcase.py +94 -0
  52. crosshair/fnutil.py +391 -0
  53. crosshair/fnutil_test.py +75 -0
  54. crosshair/fuzz_core_test.py +516 -0
  55. crosshair/libimpl/__init__.py +0 -0
  56. crosshair/libimpl/arraylib.py +161 -0
  57. crosshair/libimpl/binascii_ch_test.py +30 -0
  58. crosshair/libimpl/binascii_test.py +67 -0
  59. crosshair/libimpl/binasciilib.py +150 -0
  60. crosshair/libimpl/bisectlib_test.py +23 -0
  61. crosshair/libimpl/builtinslib.py +5228 -0
  62. crosshair/libimpl/builtinslib_ch_test.py +1191 -0
  63. crosshair/libimpl/builtinslib_test.py +3735 -0
  64. crosshair/libimpl/codecslib.py +86 -0
  65. crosshair/libimpl/codecslib_test.py +86 -0
  66. crosshair/libimpl/collectionslib.py +264 -0
  67. crosshair/libimpl/collectionslib_ch_test.py +252 -0
  68. crosshair/libimpl/collectionslib_test.py +332 -0
  69. crosshair/libimpl/copylib.py +23 -0
  70. crosshair/libimpl/copylib_test.py +18 -0
  71. crosshair/libimpl/datetimelib.py +2559 -0
  72. crosshair/libimpl/datetimelib_ch_test.py +354 -0
  73. crosshair/libimpl/datetimelib_test.py +112 -0
  74. crosshair/libimpl/decimallib.py +5257 -0
  75. crosshair/libimpl/decimallib_ch_test.py +78 -0
  76. crosshair/libimpl/decimallib_test.py +76 -0
  77. crosshair/libimpl/encodings/__init__.py +23 -0
  78. crosshair/libimpl/encodings/_encutil.py +187 -0
  79. crosshair/libimpl/encodings/ascii.py +44 -0
  80. crosshair/libimpl/encodings/latin_1.py +40 -0
  81. crosshair/libimpl/encodings/utf_8.py +93 -0
  82. crosshair/libimpl/encodings_ch_test.py +83 -0
  83. crosshair/libimpl/fractionlib.py +16 -0
  84. crosshair/libimpl/fractionlib_test.py +80 -0
  85. crosshair/libimpl/functoolslib.py +34 -0
  86. crosshair/libimpl/functoolslib_test.py +56 -0
  87. crosshair/libimpl/hashliblib.py +30 -0
  88. crosshair/libimpl/hashliblib_test.py +18 -0
  89. crosshair/libimpl/heapqlib.py +47 -0
  90. crosshair/libimpl/heapqlib_test.py +21 -0
  91. crosshair/libimpl/importliblib.py +18 -0
  92. crosshair/libimpl/importliblib_test.py +38 -0
  93. crosshair/libimpl/iolib.py +216 -0
  94. crosshair/libimpl/iolib_ch_test.py +128 -0
  95. crosshair/libimpl/iolib_test.py +19 -0
  96. crosshair/libimpl/ipaddresslib.py +8 -0
  97. crosshair/libimpl/itertoolslib.py +44 -0
  98. crosshair/libimpl/itertoolslib_test.py +44 -0
  99. crosshair/libimpl/jsonlib.py +984 -0
  100. crosshair/libimpl/jsonlib_ch_test.py +42 -0
  101. crosshair/libimpl/jsonlib_test.py +51 -0
  102. crosshair/libimpl/mathlib.py +179 -0
  103. crosshair/libimpl/mathlib_ch_test.py +44 -0
  104. crosshair/libimpl/mathlib_test.py +67 -0
  105. crosshair/libimpl/oslib.py +7 -0
  106. crosshair/libimpl/pathliblib_test.py +10 -0
  107. crosshair/libimpl/randomlib.py +178 -0
  108. crosshair/libimpl/randomlib_test.py +120 -0
  109. crosshair/libimpl/relib.py +846 -0
  110. crosshair/libimpl/relib_ch_test.py +169 -0
  111. crosshair/libimpl/relib_test.py +493 -0
  112. crosshair/libimpl/timelib.py +72 -0
  113. crosshair/libimpl/timelib_test.py +82 -0
  114. crosshair/libimpl/typeslib.py +15 -0
  115. crosshair/libimpl/typeslib_test.py +36 -0
  116. crosshair/libimpl/unicodedatalib.py +75 -0
  117. crosshair/libimpl/unicodedatalib_test.py +42 -0
  118. crosshair/libimpl/urlliblib.py +23 -0
  119. crosshair/libimpl/urlliblib_test.py +19 -0
  120. crosshair/libimpl/weakreflib.py +13 -0
  121. crosshair/libimpl/weakreflib_test.py +69 -0
  122. crosshair/libimpl/zliblib.py +15 -0
  123. crosshair/libimpl/zliblib_test.py +13 -0
  124. crosshair/lsp_server.py +261 -0
  125. crosshair/lsp_server_test.py +30 -0
  126. crosshair/main.py +973 -0
  127. crosshair/main_test.py +543 -0
  128. crosshair/objectproxy.py +376 -0
  129. crosshair/objectproxy_test.py +41 -0
  130. crosshair/opcode_intercept.py +601 -0
  131. crosshair/opcode_intercept_test.py +304 -0
  132. crosshair/options.py +218 -0
  133. crosshair/options_test.py +10 -0
  134. crosshair/patch_equivalence_test.py +75 -0
  135. crosshair/path_cover.py +209 -0
  136. crosshair/path_cover_test.py +138 -0
  137. crosshair/path_search.py +161 -0
  138. crosshair/path_search_test.py +52 -0
  139. crosshair/pathing_oracle.py +271 -0
  140. crosshair/pathing_oracle_test.py +21 -0
  141. crosshair/pure_importer.py +27 -0
  142. crosshair/pure_importer_test.py +16 -0
  143. crosshair/py.typed +0 -0
  144. crosshair/register_contract.py +273 -0
  145. crosshair/register_contract_test.py +190 -0
  146. crosshair/simplestructs.py +1165 -0
  147. crosshair/simplestructs_test.py +283 -0
  148. crosshair/smtlib.py +24 -0
  149. crosshair/smtlib_test.py +14 -0
  150. crosshair/statespace.py +1199 -0
  151. crosshair/statespace_test.py +108 -0
  152. crosshair/stubs_parser.py +352 -0
  153. crosshair/stubs_parser_test.py +43 -0
  154. crosshair/test_util.py +329 -0
  155. crosshair/test_util_test.py +26 -0
  156. crosshair/tools/__init__.py +0 -0
  157. crosshair/tools/check_help_in_doc.py +264 -0
  158. crosshair/tools/check_init_and_setup_coincide.py +119 -0
  159. crosshair/tools/generate_demo_table.py +127 -0
  160. crosshair/tracers.py +544 -0
  161. crosshair/tracers_test.py +154 -0
  162. crosshair/type_repo.py +151 -0
  163. crosshair/unicode_categories.py +589 -0
  164. crosshair/unicode_categories_test.py +27 -0
  165. crosshair/util.py +741 -0
  166. crosshair/util_test.py +173 -0
  167. crosshair/watcher.py +307 -0
  168. crosshair/watcher_test.py +107 -0
  169. crosshair/z3util.py +76 -0
  170. crosshair/z3util_test.py +11 -0
  171. crosshair_tool-0.0.99.dist-info/METADATA +144 -0
  172. crosshair_tool-0.0.99.dist-info/RECORD +176 -0
  173. crosshair_tool-0.0.99.dist-info/WHEEL +6 -0
  174. crosshair_tool-0.0.99.dist-info/entry_points.txt +3 -0
  175. crosshair_tool-0.0.99.dist-info/licenses/LICENSE +93 -0
  176. 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)
@@ -0,0 +1,11 @@
1
+ from enum import IntEnum
2
+
3
+ from crosshair.z3util import z3IntVal
4
+
5
+
6
+ class IntSubClass(IntEnum):
7
+ FIRST = 1
8
+
9
+
10
+ def test_intval_on_int_enum():
11
+ z3IntVal(IntSubClass.FIRST)