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
@@ -0,0 +1,80 @@
1
+ import math
2
+ from fractions import Fraction
3
+
4
+ from crosshair.core import deep_realize
5
+ from crosshair.core_and_libs import proxy_for_type
6
+ from crosshair.statespace import POST_FAIL
7
+ from crosshair.test_util import check_states
8
+ from crosshair.tracers import ResumedTracing, is_tracing
9
+ from crosshair.util import CrossHairInternal
10
+
11
+
12
+ def test_fraction_realize(space):
13
+ n = proxy_for_type(int, "n")
14
+ d = proxy_for_type(int, "d")
15
+ with ResumedTracing():
16
+ space.add(d != 0)
17
+ deep_realize(Fraction(n, d))
18
+
19
+
20
+ class UserFraction(Fraction):
21
+ def __int__(self):
22
+ if not is_tracing():
23
+ raise CrossHairInternal("tracing required while in user code")
24
+ return 1
25
+
26
+ def __round__(self, *a, **kw):
27
+ if not is_tracing():
28
+ raise CrossHairInternal("tracing required while in user code")
29
+ return super().__round__(*a, **kw)
30
+
31
+
32
+ def test_user_fraction_tracing(space):
33
+ n = proxy_for_type(int, "n")
34
+ d = proxy_for_type(int, "d")
35
+ with ResumedTracing():
36
+ space.add(d != 0)
37
+ fraction = UserFraction(n, d)
38
+ round(fraction) # (works via with_realized_args)
39
+ int(fraction) # (custom interception)
40
+
41
+
42
+ def test_fraction_copy_doesnt_realize(space):
43
+ n = proxy_for_type(int, "n")
44
+ with ResumedTracing():
45
+ space.add(n >= 0)
46
+ f1 = Fraction(n, 1)
47
+ f2 = f1.__copy__()
48
+ assert space.is_possible(f2.numerator == 2)
49
+ assert space.is_possible(f2.numerator == 3)
50
+
51
+
52
+ def test_fraction_can_be_one_half() -> None:
53
+ def f(f: Fraction):
54
+ """post:_"""
55
+ return f != Fraction(1, 2)
56
+
57
+ check_states(f, POST_FAIL)
58
+
59
+
60
+ def test_int_from_fraction(space) -> None:
61
+ n = proxy_for_type(int, "n")
62
+ d = proxy_for_type(int, "d")
63
+ with ResumedTracing():
64
+ space.add(d > 0)
65
+ space.add(n == d * 3)
66
+ f = Fraction(n, d)
67
+ assert space.is_possible(d == 3)
68
+ assert not space.is_possible(f.denominator != 1)
69
+ truncated_fraction = int(f)
70
+ assert space.is_possible(truncated_fraction == 3)
71
+ assert not space.is_possible(truncated_fraction != 3)
72
+
73
+
74
+ def test_fraction_ceil_does_not_explode(space) -> None:
75
+ f = proxy_for_type(Fraction, "f")
76
+ with ResumedTracing():
77
+ math.ceil(f)
78
+
79
+
80
+ # TODO: The math module is patched with deep_realize, but many of the usual operators may not work. Test.
@@ -0,0 +1,34 @@
1
+ from functools import _lru_cache_wrapper, partial, reduce, update_wrapper, wraps
2
+
3
+ from crosshair.core import register_patch
4
+
5
+ # TODO: deal with lru_cache (note it needs to be intercepted at import-time)
6
+
7
+
8
+ def _partial(func, *a1, **kw1):
9
+ if callable(func):
10
+ # We make a do-nothing wrapper to ensure that the tracer has a crack
11
+ # at this function when it is called.
12
+ def wrapper(*a2, **kw2):
13
+ return func(*a2, **kw2)
14
+
15
+ update_wrapper(wrapper, func)
16
+ return partial(wrapper, *a1, **kw1)
17
+ else:
18
+ raise TypeError
19
+
20
+
21
+ def _reduce(function, *a, **kw):
22
+ return reduce(lambda x, y: function(x, y), *a, **kw)
23
+
24
+
25
+ def make_registrations():
26
+ register_patch(partial, _partial)
27
+ register_patch(reduce, _reduce)
28
+
29
+ def call_with_skipped_cache(self, *a, **kw):
30
+ if not isinstance(self, _lru_cache_wrapper):
31
+ raise TypeError
32
+ return self.__wrapped__(*a, **kw)
33
+
34
+ register_patch(_lru_cache_wrapper.__call__, call_with_skipped_cache)
@@ -0,0 +1,56 @@
1
+ import functools
2
+ import inspect
3
+
4
+ from crosshair.core import proxy_for_type, standalone_statespace
5
+ from crosshair.libimpl.builtinslib import LazyIntSymbolicStr
6
+ from crosshair.tracers import NoTracing, ResumedTracing
7
+
8
+
9
+ def test_partial(space):
10
+ abc = LazyIntSymbolicStr(list(map(ord, "abc")))
11
+ xyz = LazyIntSymbolicStr(list(map(ord, "xyz")))
12
+ with ResumedTracing():
13
+ joiner = functools.partial(str.join, ",")
14
+ ret = joiner([abc, xyz])
15
+ assert ret == "abc,xyz"
16
+
17
+
18
+ def test_partial_is_interceptable(space):
19
+ x = proxy_for_type(str, "x")
20
+ y = proxy_for_type(str, "y")
21
+ with ResumedTracing():
22
+ joiner = functools.partial(str.startswith, x)
23
+ # Ensure we don't explode
24
+ list(map(joiner, ["foo", y]))
25
+
26
+
27
+ def test_partial_arg_is_inspectable(space):
28
+ with ResumedTracing():
29
+ joiner = functools.partial(str.join, ",")
30
+ assert isinstance(joiner, functools.partial)
31
+ assert inspect.getdoc(joiner.func) == inspect.getdoc(str.join)
32
+
33
+
34
+ def test_reduce():
35
+ with standalone_statespace as space:
36
+ with NoTracing():
37
+ string = LazyIntSymbolicStr(list(map(ord, "12 oofoo 12")))
38
+ tostrip = LazyIntSymbolicStr(list(map(ord, "2")))
39
+ ret = functools.reduce(str.strip, [string, "1", "2"])
40
+ assert ret == " oofoo 1"
41
+
42
+
43
+ _global_state = [42]
44
+
45
+
46
+ @functools.lru_cache()
47
+ def whaa(x: int) -> int:
48
+ _global_state[0] += 1
49
+ return _global_state[0]
50
+
51
+
52
+ def test_lru_cache_is_ignored():
53
+ with standalone_statespace as space:
54
+ assert whaa(0) == 43
55
+ assert whaa(1) == 44
56
+ assert whaa(1) == 45
@@ -0,0 +1,30 @@
1
+ import hashlib
2
+ import sys
3
+
4
+ from crosshair.core import register_patch, with_realized_args
5
+
6
+
7
+ def make_registrations():
8
+ if sys.version_info < (3, 12):
9
+ # As of Python 3.12, SymbolicBytes can implement __buffer__() to be compatible
10
+ # with hash functions. Prior to that, we patch them manually:
11
+
12
+ to_patch = {hashlib.new: None} # we don't use a set so that the patch order
13
+ # is deterministic, which matters for the patch_equivalence_test when
14
+ # run under pytest -n
15
+ for algo_string in sorted(hashlib.algorithms_available):
16
+ hash_constructor = getattr(hashlib, algo_string, None)
17
+ if hash_constructor is not None:
18
+ to_patch[hash_constructor] = None
19
+ try:
20
+ example_instance = hashlib.new(algo_string)
21
+ except ValueError:
22
+ if sys.version_info < (3, 9):
23
+ # in 3.8, some "available" algorithms aren't available
24
+ continue
25
+ else:
26
+ raise
27
+ update_method = getattr((type(example_instance)), "update")
28
+ to_patch[update_method] = None
29
+ for fn in to_patch:
30
+ register_patch(fn, with_realized_args(fn))
@@ -0,0 +1,18 @@
1
+ import hashlib
2
+
3
+ from crosshair.core import proxy_for_type, standalone_statespace
4
+
5
+
6
+ def test_sha384():
7
+ with standalone_statespace:
8
+ x = proxy_for_type(bytes, "x")
9
+ hashlib.new("sha384", x)
10
+ hashlib.sha384(x)
11
+
12
+
13
+ def test_blake_via_update():
14
+ with standalone_statespace:
15
+ x = proxy_for_type(bytes, "x")
16
+ h = hashlib.blake2b()
17
+ h.update(x)
18
+ h.hexdigest()
@@ -0,0 +1,47 @@
1
+ import functools
2
+ import heapq
3
+ import types
4
+
5
+ import _heapq
6
+
7
+ from crosshair.core import register_patch
8
+ from crosshair.util import debug, imported_alternative, name_of_type
9
+
10
+
11
+ def _check_first_arg_is_list(fn):
12
+ functools.wraps(fn)
13
+
14
+ def wrapper(heap, *a, **kw):
15
+ if not isinstance(heap, list):
16
+ raise TypeError(
17
+ f"{fn.__name__} argument must be list, not {name_of_type(heap)}"
18
+ )
19
+ return fn(heap, *a, **kw)
20
+
21
+ return wrapper
22
+
23
+
24
+ def make_registrations():
25
+ native_funcs = [
26
+ "_heapify_max",
27
+ "_heappop_max",
28
+ "_heapreplace_max",
29
+ "heapify",
30
+ "heappop",
31
+ "heappush",
32
+ "heappushpop",
33
+ "heapreplace",
34
+ ]
35
+ with imported_alternative("heapq", ("_heapq",)):
36
+
37
+ # The pure python version doesn't always check argument types:
38
+ heapq.heappush = heapq.heappush
39
+ heapq.heappop = _check_first_arg_is_list(heapq.heappop)
40
+
41
+ pure_fns = {name: getattr(heapq, name) for name in native_funcs}
42
+ for name in native_funcs:
43
+ native_fn = getattr(heapq, name)
44
+ pure_fn = pure_fns[name]
45
+ assert isinstance(native_fn, types.BuiltinFunctionType)
46
+ assert isinstance(pure_fn, types.FunctionType)
47
+ register_patch(native_fn, _check_first_arg_is_list(pure_fn))
@@ -0,0 +1,21 @@
1
+ import heapq
2
+ import sys
3
+ from typing import List
4
+
5
+ import pytest
6
+
7
+ from crosshair.core import proxy_for_type
8
+ from crosshair.tracers import ResumedTracing
9
+
10
+
11
+ # TODO https://github.com/pschanely/CrossHair/issues/298
12
+ @pytest.mark.skip(
13
+ reason="heapq get reloaded somehow in parallel ci run, ruining the intercepts",
14
+ )
15
+ def test_heapify(space):
16
+ items = proxy_for_type(List[int], "items")
17
+
18
+ with ResumedTracing():
19
+ space.add(len(items) == 3)
20
+ heapq.heapify(items)
21
+ assert not space.is_possible(items[0] > items[1])
@@ -0,0 +1,18 @@
1
+ from importlib._bootstrap import _find_and_load_unlocked # type: ignore
2
+
3
+ from crosshair import register_patch
4
+ from crosshair.tracers import NoTracing
5
+
6
+
7
+ # Do not import with tracing enabled.
8
+ # (it's expensive, and won't behave deterministically)
9
+ #
10
+ # NOTE: The importlib._bootstrap._find_and_load_unlocked entry point covers both the
11
+ # regular import statement, as well as loads via the importlib module.
12
+ def __find_and_load_unlocked(*a, **kw):
13
+ with NoTracing():
14
+ return _find_and_load_unlocked(*a, **kw)
15
+
16
+
17
+ def make_registrations() -> None:
18
+ register_patch(_find_and_load_unlocked, __find_and_load_unlocked)
@@ -0,0 +1,38 @@
1
+ import subprocess
2
+ import sys
3
+
4
+ from crosshair.main import Path, textwrap
5
+ from crosshair.test_util import simplefs
6
+
7
+ DYNAMIC_IMPORT = {
8
+ "__init__.py": "",
9
+ "outer.py": textwrap.dedent(
10
+ """\
11
+ def outerfn(x: int) -> int:
12
+ ''' post: _ == x '''
13
+ from .innerx import innerfn
14
+ return innerfn(x)
15
+ """
16
+ ),
17
+ "innerx.py": textwrap.dedent(
18
+ """
19
+ from crosshair.tracers import is_tracing
20
+ assert not is_tracing()
21
+ def innerfn(x: int) -> int:
22
+ return x
23
+ """
24
+ ),
25
+ }
26
+
27
+
28
+ def test_dynamic_import(tmp_path: Path):
29
+ # This imports another module while checking.
30
+ # The inner module asserts that tracing is not enabled.
31
+ simplefs(tmp_path, DYNAMIC_IMPORT)
32
+ ret = subprocess.run(
33
+ [sys.executable, "-m", "crosshair", "check", str(tmp_path / "outer.py")],
34
+ stdin=subprocess.DEVNULL,
35
+ capture_output=True,
36
+ text=True,
37
+ )
38
+ assert (ret.returncode, ret.stdout, ret.stderr) == (0, "", "")
@@ -0,0 +1,216 @@
1
+ import re
2
+ from io import SEEK_CUR, SEEK_END, SEEK_SET, StringIO, TextIOBase
3
+ from typing import Optional, Tuple, Union
4
+
5
+ from crosshair import ResumedTracing, SymbolicFactory, register_type
6
+ from crosshair.core import realize, register_patch
7
+ from crosshair.tracers import NoTracing
8
+ from crosshair.util import CrossHairValue, IgnoreAttempt
9
+
10
+ _UNIVERSAL_NEWLINE_RE = re.compile(r"(\r\n|\r|\n)")
11
+
12
+
13
+ class BackedStringIO(TextIOBase, CrossHairValue):
14
+ _contents: str
15
+ _pos: int
16
+ _discovered_newlines: set
17
+ _newline_mode: Optional[str]
18
+
19
+ def __init__(
20
+ self,
21
+ initial_value: Optional[str] = None,
22
+ newline: Optional[str] = "\n",
23
+ pos: int = 0,
24
+ ):
25
+ if not (isinstance(newline, (str, type(None)))):
26
+ raise TypeError
27
+ if newline not in (None, "", "\n", "\r", "\r\n"):
28
+ raise ValueError
29
+ if initial_value is None:
30
+ initial_value = ""
31
+ if not (isinstance(initial_value, str)):
32
+ raise TypeError
33
+ if pos < 0:
34
+ raise ValueError
35
+ self._newline_mode = newline
36
+ self._discovered_newlines = set()
37
+ self._pos = pos
38
+ self._contents = self._replace_newlines(initial_value) if initial_value else ""
39
+
40
+ def __repr__(self):
41
+ contents, newline_mode, pos = self._contents, self._newline_mode, self._pos
42
+ if pos == 0:
43
+ if newline_mode == "\n":
44
+ return f"BackedStringIO({contents!r})"
45
+ else:
46
+ return f"BackedStringIO({contents!r}, {newline_mode!r})"
47
+ return (
48
+ f"BackedStringIO({contents!r}, newline_mode={newline_mode!r}, pos={pos!r})"
49
+ )
50
+
51
+ def __ch_pytype__(self):
52
+ return StringIO
53
+
54
+ def __ch_realize__(self):
55
+ if self.closed:
56
+ raise ValueError
57
+ contents, newline_mode = realize(self._contents), realize(self._newline_mode)
58
+ with NoTracing():
59
+ sio = StringIO(contents, newline_mode)
60
+ sio.seek(realize(self._pos))
61
+ return sio
62
+
63
+ @property
64
+ def newlines(self) -> Union[None, str, Tuple[str, ...]]: # type: ignore
65
+ discovered = self._discovered_newlines
66
+ # Fiddly! Trying to preserve the static tuple ordering that CPython has:
67
+ ret = tuple(nl for nl in ("\r", "\n", "\r\n") if nl in discovered)
68
+ if len(ret) > 1:
69
+ return ret
70
+ if len(ret) == 1:
71
+ return ret[0]
72
+ return None
73
+
74
+ def _replace_newlines(self, string: str) -> str:
75
+ newline_mode = self._newline_mode
76
+ if newline_mode is None:
77
+
78
+ def replace(match: re.Match) -> str:
79
+ self._discovered_newlines.add(match.group())
80
+ return "\n"
81
+
82
+ return _UNIVERSAL_NEWLINE_RE.sub(replace, string)
83
+ elif newline_mode == "":
84
+ self._discovered_newlines.update(_UNIVERSAL_NEWLINE_RE.findall(string))
85
+ return string
86
+ elif newline_mode == "\n":
87
+ return string
88
+ else:
89
+ return string.replace("\n", newline_mode)
90
+
91
+ def flush(self) -> None:
92
+ return
93
+
94
+ def getvalue(self) -> str:
95
+ return self._contents
96
+
97
+ def read(self, amount: Optional[int] = None) -> str:
98
+ if self.closed:
99
+ raise ValueError
100
+ if amount is None:
101
+ ret = self._contents[self._pos :]
102
+ else:
103
+ ret = self._contents[self._pos : self._pos + amount]
104
+ self._pos += len(ret)
105
+ return ret
106
+
107
+ def readable(self) -> bool:
108
+ if self.closed:
109
+ raise ValueError
110
+ return True
111
+
112
+ def readline(self, limit: Optional[int] = None) -> str: # type: ignore
113
+ if self.closed:
114
+ raise ValueError
115
+ contents, pos = self._contents, self._pos
116
+ if limit is None:
117
+ limit = len(contents)
118
+ if self._newline_mode == "":
119
+ # All other modes would have normalized the contents to \n already.
120
+ for match in _UNIVERSAL_NEWLINE_RE.finditer(contents, pos, limit):
121
+ self._pos = match.end()
122
+ return contents[pos : match.end()]
123
+ self._pos = limit
124
+ return contents[pos:limit]
125
+ else:
126
+ nl = self._newline_mode or "\n"
127
+ nl_size = len(nl)
128
+ idx = contents.find(nl, pos, limit)
129
+ if idx == -1:
130
+ self._pos = limit
131
+ return contents[pos:limit]
132
+ else:
133
+ self._pos = idx + nl_size
134
+ return contents[pos : idx + nl_size]
135
+
136
+ def write(self, string: str) -> int:
137
+ if self.closed:
138
+ raise ValueError
139
+ contents, pos = self._contents, self._pos
140
+ contentslen = len(contents)
141
+ if not string:
142
+ return 0
143
+ writestr = self._replace_newlines(string)
144
+ writelen = len(writestr)
145
+ if pos > contentslen:
146
+ self._contents += "\u0000" * (pos - contentslen)
147
+ contentslen = pos
148
+ if pos == contentslen:
149
+ self._contents += writestr
150
+ else:
151
+ self._contents = contents[:pos] + writestr + contents[pos + writelen :]
152
+ self._pos = pos + writelen
153
+ # Don't return `writelen` because all the input characters were "written":
154
+ return len(string)
155
+
156
+ def seek(self, amount: int, whence: int = SEEK_SET) -> int:
157
+ if self.closed:
158
+ raise ValueError
159
+ if whence == SEEK_CUR:
160
+ if amount != 0:
161
+ raise OSError
162
+ pos = self._pos + amount
163
+ elif whence == SEEK_END:
164
+ if amount != 0:
165
+ raise OSError
166
+ pos = len(self._contents) + amount
167
+ elif whence == SEEK_SET:
168
+ if amount < 0:
169
+ raise ValueError
170
+ pos = amount
171
+ else:
172
+ raise ValueError
173
+ self._pos = pos
174
+ return pos
175
+
176
+ def seekable(self) -> bool:
177
+ if self.closed:
178
+ raise ValueError
179
+ return True
180
+
181
+ def tell(self) -> int:
182
+ if self.closed:
183
+ raise ValueError
184
+ return self._pos
185
+
186
+ def truncate(self, size: Optional[int] = None) -> int:
187
+ if self.closed:
188
+ raise ValueError
189
+ if size is None:
190
+ size = self._pos
191
+ self._contents = self._contents[:size]
192
+ return size
193
+
194
+ def writable(self) -> bool:
195
+ if self.closed:
196
+ raise ValueError
197
+ return True
198
+
199
+
200
+ def make_string_io(factory: SymbolicFactory) -> BackedStringIO:
201
+ contents = factory(str)
202
+ newline_mode = factory(Optional[str])
203
+ with ResumedTracing():
204
+ if newline_mode not in (None, "", "\n", "\r", "\r\n"):
205
+ raise IgnoreAttempt
206
+ return BackedStringIO(contents, newline_mode)
207
+
208
+
209
+ def _string_io(initial_value: str = "", newline="\n"):
210
+ return BackedStringIO(initial_value, newline)
211
+
212
+
213
+ def make_registrations() -> None:
214
+ register_type(StringIO, make_string_io)
215
+ register_patch(StringIO, _string_io)
216
+ # TODO: register_type io.TextIO, BytesIO, ...
@@ -0,0 +1,128 @@
1
+ import sys
2
+ from io import StringIO
3
+ from typing import List, Optional, Tuple, Union
4
+
5
+ import pytest # type: ignore
6
+
7
+ from crosshair.core_and_libs import MessageType, analyze_function, run_checkables
8
+ from crosshair.libimpl.iolib import BackedStringIO
9
+ from crosshair.test_util import compare_returns
10
+
11
+
12
+ def _do_something(s: Union[StringIO, BackedStringIO], opname: str) -> object:
13
+ if opname == "closed":
14
+ return s.closed
15
+ elif opname == "flush":
16
+ s.flush()
17
+ elif opname == "read":
18
+ return s.read()
19
+ elif opname == "readlines":
20
+ return s.readlines()
21
+ elif opname == "readable":
22
+ return s.readable()
23
+ elif opname == "seek":
24
+ return s.seek(1)
25
+ elif opname == "seekable":
26
+ return s.seekable()
27
+ elif opname == "tell":
28
+ return s.tell()
29
+ elif opname == "truncate":
30
+ return s.truncate()
31
+ elif opname == "writable":
32
+ return s.writable()
33
+ elif opname == "write":
34
+ return s.write("")
35
+ return None
36
+
37
+
38
+ def check_stringio_readlines(s: StringIO, hint: int):
39
+ """post: _"""
40
+
41
+ def readlines(s, hint: int):
42
+ return s.readlines(hint)
43
+
44
+ return compare_returns(readlines, s, hint)
45
+
46
+
47
+ def check_stringio_writelines(s: StringIO, lines: List[str]):
48
+ """post: _"""
49
+
50
+ def writelines(s, lines: List[str]):
51
+ retval = s.writelines(lines)
52
+ return (retval, s.tell(), s.getvalue())
53
+
54
+ return compare_returns(writelines, s, lines)
55
+
56
+
57
+ def check_stringio_seek1(s: StringIO, o1: int, w1: int):
58
+ """post: _"""
59
+
60
+ def seek_double(s, o1: int, w1: int) -> int:
61
+ s.seek(o1, w1)
62
+ return s.tell()
63
+
64
+ return compare_returns(seek_double, s, o1, w1)
65
+
66
+
67
+ def check_stringio_seek_seek(s: StringIO, o1: int, w1: int, o2: int, w2: int):
68
+ """post: _"""
69
+
70
+ def seek_seek(s, o1: int, w1: int, o2: int, w2: int) -> int:
71
+ s.seek(o1, w1)
72
+ s.seek(o2, w2)
73
+ return s.tell()
74
+
75
+ return compare_returns(seek_seek, s, o1, w1, o2, w2)
76
+
77
+
78
+ def check_stringio_seek_write(s: StringIO, o1: int, w1: int, ws: str):
79
+ """post: _"""
80
+ # crosshair: max_uninteresting_iterations=15
81
+
82
+ def seek_write(s, o1: int, w1: int, ws: str) -> Tuple[int, int, str]:
83
+ s.seek(o1, w1)
84
+ retval = s.write(ws)
85
+ return (retval, s.tell(), s.getvalue())
86
+
87
+ return compare_returns(seek_write, s, o1, w1, ws)
88
+
89
+
90
+ def check_stringio_write_newlines(newline_mode: str, ws: str):
91
+ """post: _"""
92
+
93
+ def write_newlines(newline_mode: Optional[str], ws: str):
94
+ if newline_mode in (None, "", "\n", "\r", "\r\n"):
95
+ s = BackedStringIO("", newline_mode)
96
+ s.write(ws)
97
+ return s.newlines
98
+
99
+ return compare_returns(write_newlines, newline_mode, ws)
100
+
101
+
102
+ def check_stringio_simple_operation(s: StringIO, opname: str):
103
+ """post: _"""
104
+
105
+ def simple_operation(s, opname: str) -> object:
106
+ return _do_something(s, opname)
107
+
108
+ return compare_returns(simple_operation, s, opname)
109
+
110
+
111
+ def check_stringio_operation_while_closed(s: StringIO, opname: str):
112
+ """post: _"""
113
+
114
+ def closed_operation(s, opname: str) -> object:
115
+ s.close()
116
+ return _do_something(s, opname)
117
+
118
+ return compare_returns(closed_operation, s, opname)
119
+
120
+
121
+ # This is the only real test definition.
122
+ # It runs crosshair on each of the "check" functions defined above.
123
+ @pytest.mark.parametrize("fn_name", [fn for fn in dir() if fn.startswith("check_")])
124
+ def test_builtin(fn_name: str) -> None:
125
+ fn = getattr(sys.modules[__name__], fn_name)
126
+ messages = run_checkables(analyze_function(fn))
127
+ errors = [m for m in messages if m.state > MessageType.PRE_UNSAT]
128
+ assert errors == []