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,314 @@
1
+ import copy
2
+ import dataclasses
3
+ import dis
4
+ import enum
5
+ import inspect
6
+ import sys
7
+ import time
8
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
9
+
10
+ from crosshair import IgnoreAttempt
11
+ from crosshair.condition_parser import condition_parser
12
+ from crosshair.core import ExceptionFilter, Patched, deep_realize, gen_args
13
+ from crosshair.fnutil import FunctionInfo
14
+ from crosshair.options import AnalysisOptions
15
+ from crosshair.statespace import (
16
+ CallAnalysis,
17
+ RootNode,
18
+ StateSpace,
19
+ StateSpaceContext,
20
+ VerificationStatus,
21
+ )
22
+ from crosshair.test_util import flexible_equal
23
+ from crosshair.tracers import (
24
+ COMPOSITE_TRACER,
25
+ CoverageResult,
26
+ CoverageTracingModule,
27
+ NoTracing,
28
+ PushedModule,
29
+ ResumedTracing,
30
+ )
31
+ from crosshair.util import CrosshairUnsupported, IgnoreAttempt, UnexploredPath, debug
32
+
33
+
34
+ class ExceptionEquivalenceType(enum.Enum):
35
+ ALL = "ALL"
36
+ SAME_TYPE = "SAME_TYPE"
37
+ TYPE_AND_MESSAGE = "TYPE_AND_MESSAGE"
38
+
39
+
40
+ @dataclasses.dataclass
41
+ class Result:
42
+ return_repr: str
43
+ error: Optional[str]
44
+ post_execution_args: Dict[str, str]
45
+
46
+ def get_differing_arg_mutations(self, other: "Result") -> Set[str]:
47
+ args1 = self.post_execution_args
48
+ args2 = other.post_execution_args
49
+ differing_args: Set[str] = set()
50
+ for arg in set(args1.keys()) | args2.keys():
51
+ if arg in args1 and arg in args2 and args1[arg] != args2[arg]:
52
+ differing_args.add(arg)
53
+ return differing_args
54
+
55
+ def describe(self, args_to_show: Set[str]) -> str:
56
+ ret = []
57
+ if self.error is not None:
58
+ ret.append(f"raises {self.error}")
59
+ if self.return_repr != "None":
60
+ ret.append(f"returns {self.return_repr}")
61
+ if args_to_show:
62
+ if ret:
63
+ ret.append(", ")
64
+ ret.append("after execution ")
65
+ ret.append(
66
+ ", ".join(
67
+ f"{arg}={self.post_execution_args[arg]}" for arg in args_to_show
68
+ )
69
+ )
70
+ # last resort, be explicit about returning none:
71
+ if not ret:
72
+ ret.append("returns None")
73
+ return "".join(ret)
74
+
75
+
76
+ def describe_behavior(
77
+ fn: Callable, args: inspect.BoundArguments
78
+ ) -> Tuple[Any, Optional[BaseException]]:
79
+ with ExceptionFilter() as efilter:
80
+ ret = fn(*args.args, **args.kwargs)
81
+ return (ret, None)
82
+ if efilter.user_exc is not None:
83
+ exc = efilter.user_exc[0]
84
+ debug("user-level exception found", repr(exc), *efilter.user_exc[1])
85
+ return (None, exc)
86
+ if efilter.ignore:
87
+ return (None, IgnoreAttempt())
88
+ assert False
89
+
90
+
91
+ @dataclasses.dataclass
92
+ class BehaviorDiff:
93
+ args: Dict[str, str]
94
+ result1: Result
95
+ result2: Result
96
+ coverage1: CoverageResult
97
+ coverage2: CoverageResult
98
+
99
+ def reverse(self) -> "BehaviorDiff":
100
+ return BehaviorDiff(
101
+ self.args, self.result2, self.result1, self.coverage2, self.coverage1
102
+ )
103
+
104
+
105
+ def diff_scorer(
106
+ check_opcodes1: Set[int], check_opcodes2: Set[int]
107
+ ) -> Callable[[BehaviorDiff], Tuple[float, float]]:
108
+ """
109
+ Create a function to score the usefulness of behavior diffs.
110
+
111
+ We aim for a minimal number of examples that gives as much coverage of the
112
+ differing opcodes as possible.
113
+ We break ties on smaller examples. (repr-string-length-wise)
114
+ """
115
+ pass # for pydocstyle
116
+
117
+ def scorer(diff: BehaviorDiff) -> Tuple[float, float]:
118
+ coverage1 = diff.coverage1
119
+ coverage2 = diff.coverage2
120
+ total_opcodes = len(coverage1.all_offsets) + len(coverage2.all_offsets)
121
+ cover1 = len(coverage1.offsets_covered & check_opcodes1)
122
+ cover2 = len(coverage2.offsets_covered & check_opcodes2)
123
+ cover_score = (cover1 + cover2) / total_opcodes
124
+ strlen_score = len(str(diff))
125
+ return (cover_score, strlen_score)
126
+
127
+ return scorer
128
+
129
+
130
+ def diff_behavior(
131
+ ctxfn1: FunctionInfo,
132
+ ctxfn2: FunctionInfo,
133
+ options: AnalysisOptions,
134
+ exception_equivalence: ExceptionEquivalenceType = ExceptionEquivalenceType.TYPE_AND_MESSAGE,
135
+ ) -> Union[str, List[BehaviorDiff]]:
136
+ fn1, sig1 = ctxfn1.callable()
137
+ fn2, sig2 = ctxfn2.callable()
138
+ debug("Resolved signature:", sig1)
139
+ all_diffs: List[BehaviorDiff] = []
140
+ half1, half2 = options.split_limits(0.5)
141
+ with condition_parser(
142
+ options.analysis_kind
143
+ ), Patched(), COMPOSITE_TRACER, NoTracing():
144
+ # We attempt both orderings of functions. This helps by:
145
+ # (1) avoiding code path explosions in one of the functions
146
+ # (2) using both signatures (in case they differ)
147
+ all_diffs.extend(
148
+ diff_behavior_with_signature(fn1, fn2, sig1, half1, exception_equivalence)
149
+ )
150
+ all_diffs.extend(
151
+ diff.reverse()
152
+ for diff in diff_behavior_with_signature(
153
+ fn2, fn1, sig2, half2, exception_equivalence
154
+ )
155
+ )
156
+ debug("diff candidates:", all_diffs)
157
+ # greedily pick results:
158
+ result_diffs = []
159
+ opcodeset1 = set(i.offset for i in dis.get_instructions(fn1.__code__))
160
+ opcodeset2 = set(i.offset for i in dis.get_instructions(fn2.__code__))
161
+ while all_diffs:
162
+ scorer = diff_scorer(opcodeset1, opcodeset2)
163
+ selection = max(all_diffs, key=scorer)
164
+ (num_opcodes, _) = scorer(selection)
165
+ debug("Considering input", selection.args, " num opcodes=", num_opcodes)
166
+ if num_opcodes == 0:
167
+ break
168
+ all_diffs.remove(selection)
169
+ result_diffs.append(selection)
170
+ coverage1, coverage2 = selection.coverage1, selection.coverage2
171
+ if coverage1 is not None and coverage2 is not None:
172
+ opcodeset1 -= coverage1.offsets_covered
173
+ opcodeset2 -= coverage2.offsets_covered
174
+ return result_diffs
175
+
176
+
177
+ def diff_behavior_with_signature(
178
+ fn1: Callable,
179
+ fn2: Callable,
180
+ sig: inspect.Signature,
181
+ options: AnalysisOptions,
182
+ exception_equivalence: ExceptionEquivalenceType,
183
+ ) -> Iterable[BehaviorDiff]:
184
+ search_root = RootNode()
185
+ condition_start = time.monotonic()
186
+ max_uninteresting_iterations = options.get_max_uninteresting_iterations()
187
+ for i in range(1, options.max_iterations):
188
+ debug("Iteration ", i)
189
+ itr_start = time.monotonic()
190
+ if itr_start > condition_start + options.per_condition_timeout:
191
+ debug(
192
+ "Stopping due to --per_condition_timeout=",
193
+ options.per_condition_timeout,
194
+ )
195
+ return
196
+ options.incr("num_paths")
197
+ per_path_timeout = options.get_per_path_timeout()
198
+ space = StateSpace(
199
+ execution_deadline=itr_start + per_path_timeout,
200
+ model_check_timeout=per_path_timeout / 2,
201
+ search_root=search_root,
202
+ )
203
+ with StateSpaceContext(space):
204
+ output = None
205
+ try:
206
+ with ResumedTracing():
207
+ (verification_status, output) = run_iteration(
208
+ fn1, fn2, sig, space, exception_equivalence
209
+ )
210
+ except IgnoreAttempt:
211
+ verification_status = None
212
+ except UnexploredPath:
213
+ verification_status = VerificationStatus.UNKNOWN
214
+ debug("Verification status:", verification_status)
215
+ top_analysis, exhausted = space.bubble_status(
216
+ CallAnalysis(verification_status)
217
+ )
218
+ if output:
219
+ yield output
220
+ if exhausted:
221
+ debug("Stopping due to code path exhaustion. (yay!)")
222
+ options.incr("exhaustion")
223
+ break
224
+ if max_uninteresting_iterations != sys.maxsize:
225
+ iters_since_discovery = getattr(
226
+ search_root.pathing_oracle, "iters_since_discovery"
227
+ )
228
+ assert isinstance(iters_since_discovery, int)
229
+ debug("iters_since_discovery", iters_since_discovery)
230
+ if iters_since_discovery > max_uninteresting_iterations:
231
+ debug(
232
+ "Stopping due to --max_uninteresting_iterations=",
233
+ max_uninteresting_iterations,
234
+ )
235
+ break
236
+
237
+
238
+ def check_exception_equivalence(
239
+ exception_equivalence_type: ExceptionEquivalenceType,
240
+ exc1: Optional[BaseException],
241
+ exc2: Optional[BaseException],
242
+ ) -> bool:
243
+ if exc1 is not None and exc2 is not None:
244
+ if exception_equivalence_type == ExceptionEquivalenceType.ALL:
245
+ return True
246
+ elif exception_equivalence_type == ExceptionEquivalenceType.SAME_TYPE:
247
+ return type(exc1) == type(exc2)
248
+ elif exception_equivalence_type == ExceptionEquivalenceType.TYPE_AND_MESSAGE:
249
+ return repr(exc1) == repr(exc2)
250
+ else:
251
+ raise CrosshairUnsupported("Invalid exception_equivalence type")
252
+ else:
253
+ return (exc1 is None) and (exc2 is None)
254
+
255
+
256
+ def run_iteration(
257
+ fn1: Callable,
258
+ fn2: Callable,
259
+ sig: inspect.Signature,
260
+ space: StateSpace,
261
+ exception_equivalence: ExceptionEquivalenceType,
262
+ ) -> Tuple[Optional[VerificationStatus], Optional[BehaviorDiff]]:
263
+ with NoTracing():
264
+ original_args = gen_args(sig)
265
+ args1 = copy.deepcopy(original_args)
266
+ args2 = copy.deepcopy(original_args)
267
+
268
+ with NoTracing():
269
+ coverage_manager = CoverageTracingModule(fn1, fn2)
270
+ with ExceptionFilter() as efilter, PushedModule(coverage_manager):
271
+ return1, exc1 = describe_behavior(fn1, args1)
272
+ return2, exc2 = describe_behavior(fn2, args2)
273
+ if (
274
+ flexible_equal(return1, return2)
275
+ and flexible_equal(args1.arguments, args2.arguments)
276
+ and check_exception_equivalence(exception_equivalence, exc1, exc2)
277
+ ):
278
+ # Functions are equivalent if both have the same result,
279
+ # and deemed to have the same kind of error.
280
+ space.detach_path()
281
+ debug("Functions equivalent")
282
+ return (VerificationStatus.CONFIRMED, None)
283
+ space.detach_path()
284
+ debug("Functions differ")
285
+ realized_args = {
286
+ k: repr(deep_realize(v)) for (k, v) in original_args.arguments.items()
287
+ }
288
+ post_execution_args1 = {
289
+ k: repr(deep_realize(v)) for k, v in args1.arguments.items()
290
+ }
291
+ post_execution_args2 = {
292
+ k: repr(deep_realize(v)) for k, v in args2.arguments.items()
293
+ }
294
+ diff = BehaviorDiff(
295
+ realized_args,
296
+ Result(
297
+ repr(deep_realize(return1)),
298
+ repr(deep_realize(exc1)) if exc1 is not None else None,
299
+ post_execution_args1,
300
+ ),
301
+ Result(
302
+ repr(deep_realize(return2)),
303
+ repr(deep_realize(exc2)) if exc2 is not None else None,
304
+ post_execution_args2,
305
+ ),
306
+ coverage_manager.get_results(fn1),
307
+ coverage_manager.get_results(fn2),
308
+ )
309
+ return (VerificationStatus.REFUTED, diff)
310
+ if efilter.user_exc:
311
+ debug(
312
+ "User-level exception found", repr(efilter.user_exc[0]), efilter.user_exc[1]
313
+ )
314
+ return (None, None)
@@ -0,0 +1,261 @@
1
+ from typing import Callable, List, Optional
2
+
3
+ from crosshair.diff_behavior import (
4
+ BehaviorDiff,
5
+ ExceptionEquivalenceType,
6
+ diff_behavior,
7
+ )
8
+ from crosshair.fnutil import FunctionInfo, walk_qualname
9
+ from crosshair.options import DEFAULT_OPTIONS
10
+ from crosshair.util import debug, set_debug
11
+
12
+
13
+ def _foo1(x: int) -> int:
14
+ if x >= 100:
15
+ return 100
16
+ return x
17
+
18
+
19
+ foo1 = FunctionInfo.from_fn(_foo1)
20
+
21
+
22
+ def _foo2(x: int) -> int:
23
+ return min(x, 100)
24
+
25
+
26
+ foo2 = FunctionInfo.from_fn(_foo2)
27
+
28
+
29
+ def _foo3(x: int) -> int:
30
+ if x > 1000:
31
+ return 1000
32
+ elif x > 100:
33
+ return 100
34
+ else:
35
+ return x
36
+
37
+
38
+ foo3 = FunctionInfo.from_fn(_foo3)
39
+
40
+
41
+ class Base:
42
+ def foo(self):
43
+ return 10
44
+
45
+ @staticmethod
46
+ def staticfoo(x: int) -> int:
47
+ return min(x, 100)
48
+
49
+
50
+ class Derived(Base):
51
+ def foo(self):
52
+ return 11
53
+
54
+
55
+ def _sum_list_original(int_list):
56
+ count = 0
57
+ for i in int_list:
58
+ count += i
59
+ return count
60
+
61
+
62
+ def _sum_list_rewrite(int_list):
63
+ count = 0
64
+ for i in range(len(int_list)):
65
+ count += int_list[i]
66
+ return count
67
+
68
+
69
+ def _sum_list_rewrite_2(int_list):
70
+ class CustomException(Exception):
71
+ pass
72
+
73
+ try:
74
+ count = 0
75
+ for i in range(len(int_list)):
76
+ count += int_list[i]
77
+ except: # noqa E722
78
+ raise CustomException()
79
+ return count
80
+
81
+
82
+ class TestBehaviorDiff:
83
+ def test_diff_method(self):
84
+ diffs = diff_behavior(
85
+ walk_qualname(Base, "foo"),
86
+ walk_qualname(Derived, "foo"),
87
+ DEFAULT_OPTIONS.overlay(max_iterations=10),
88
+ )
89
+ assert isinstance(diffs, list)
90
+ assert [(d.result1.return_repr, d.result2.return_repr) for d in diffs] == [
91
+ ("10", "11")
92
+ ]
93
+
94
+ def test_diff_staticmethod(self):
95
+ diffs = diff_behavior(
96
+ walk_qualname(Base, "staticfoo"),
97
+ foo2,
98
+ DEFAULT_OPTIONS.overlay(max_iterations=10),
99
+ )
100
+ assert diffs == []
101
+
102
+ def test_diff_behavior_same(self) -> None:
103
+ diffs = diff_behavior(foo1, foo2, DEFAULT_OPTIONS.overlay(max_iterations=10))
104
+ assert diffs == []
105
+
106
+ def test_diff_behavior_different(self) -> None:
107
+ diffs = diff_behavior(foo1, foo3, DEFAULT_OPTIONS.overlay(max_iterations=10))
108
+ assert len(diffs) == 1
109
+ diff = diffs[0]
110
+ assert isinstance(diff, BehaviorDiff)
111
+ assert int(diff.args["x"]) > 1000
112
+ assert diff.result1.return_repr == "100"
113
+ assert diff.result2.return_repr == "1000"
114
+
115
+ def test_diff_behavior_mutation(self) -> None:
116
+ def cut_out_item1(a: List[int], i: int):
117
+ a[i : i + 1] = []
118
+
119
+ def cut_out_item2(a: List[int], i: int):
120
+ a[:] = a[:i] + a[i + 1 :]
121
+
122
+ # TODO: this takes longer than I'd like:
123
+ opts = DEFAULT_OPTIONS.overlay(max_iterations=40)
124
+ diffs = diff_behavior(
125
+ FunctionInfo.from_fn(cut_out_item1),
126
+ FunctionInfo.from_fn(cut_out_item2),
127
+ opts,
128
+ )
129
+ assert not isinstance(diffs, str)
130
+ assert len(diffs) == 1
131
+ diff = diffs[0]
132
+ assert len(diff.args["a"]) > 1
133
+ assert diff.args["i"] == "-1"
134
+
135
+ def test_example_coverage(self) -> None:
136
+ # Try to get examples that highlist the differences in the code.
137
+ # Here, we add more conditions for the `return True` path and
138
+ # another case where we used to just `return False`.
139
+ def isack1(s: str) -> bool:
140
+ if s in ("y", "yes"):
141
+ return True
142
+ return False
143
+
144
+ def isack2(s: str) -> Optional[bool]:
145
+ if s in ("y", "yes", "Y", "YES"):
146
+ return True
147
+ if s in ("n", "no", "N", "NO"):
148
+ return False
149
+ return None
150
+
151
+ diffs = diff_behavior(
152
+ FunctionInfo.from_fn(isack1),
153
+ FunctionInfo.from_fn(isack2),
154
+ DEFAULT_OPTIONS.overlay(max_iterations=20),
155
+ )
156
+ debug("diffs=", diffs)
157
+ assert not isinstance(diffs, str)
158
+ return_vals = set((d.result1.return_repr, d.result2.return_repr) for d in diffs)
159
+ assert return_vals == {("False", "None"), ("False", "True")}
160
+
161
+
162
+ def test_diff_behavior_lambda() -> None:
163
+ def f(a: Optional[Callable[[int], int]]):
164
+ if a:
165
+ return a(2) + 4
166
+ else:
167
+ return "hello"
168
+
169
+ diffs = diff_behavior(
170
+ FunctionInfo.from_fn(f),
171
+ FunctionInfo.from_fn(f),
172
+ DEFAULT_OPTIONS,
173
+ )
174
+ assert diffs == []
175
+
176
+
177
+ def test_diffbehavior_exceptions_default() -> None:
178
+ """
179
+ Default behavior of `diffbehavior` - treating exceptions as different.
180
+ """
181
+
182
+ diffs = diff_behavior(
183
+ FunctionInfo.from_fn(_sum_list_original),
184
+ FunctionInfo.from_fn(_sum_list_rewrite),
185
+ DEFAULT_OPTIONS,
186
+ )
187
+ debug("diffs=", diffs)
188
+ assert len(diffs) == 1 # finds a counter-example
189
+ assert isinstance(diffs[0], BehaviorDiff)
190
+ assert diffs[0].result1
191
+ assert isinstance(diffs[0].result1.error, str)
192
+ assert isinstance(diffs[0].result2.error, str)
193
+ assert diffs[0].result1.error.startswith("TypeError")
194
+ assert diffs[0].result2.error.startswith("TypeError")
195
+ assert (
196
+ diffs[0].result1.error != diffs[0].result2.error
197
+ ) # Both code-blocks raise a different type error
198
+
199
+
200
+ def test_diffbehavior_exceptions_same_type() -> None:
201
+ """
202
+ Treat exceptions of the same type as equivalent.
203
+ """
204
+
205
+ diffs = diff_behavior(
206
+ FunctionInfo.from_fn(_sum_list_original),
207
+ FunctionInfo.from_fn(_sum_list_rewrite),
208
+ DEFAULT_OPTIONS,
209
+ exception_equivalence=ExceptionEquivalenceType.SAME_TYPE,
210
+ )
211
+ debug("diffs=", diffs)
212
+ assert len(diffs) == 0 # No-counter example, because all TypeErrors are equal
213
+
214
+
215
+ def test_diffbehavior_exceptions_all() -> None:
216
+ """
217
+ Treat exceptions of all types as equivalent.
218
+ """
219
+
220
+ diffs = diff_behavior(
221
+ FunctionInfo.from_fn(_sum_list_original),
222
+ FunctionInfo.from_fn(_sum_list_rewrite_2),
223
+ DEFAULT_OPTIONS,
224
+ exception_equivalence=ExceptionEquivalenceType.ALL,
225
+ )
226
+ debug("diffs=", diffs)
227
+ assert len(diffs) == 0 # No-counter example, because all TypeErrors are equal
228
+
229
+
230
+ def test_diffbehavior_exceptions_same_type_different() -> None:
231
+ """
232
+ Find a counter-example when raising different exception types.
233
+ """
234
+
235
+ diffs = diff_behavior(
236
+ FunctionInfo.from_fn(_sum_list_original),
237
+ FunctionInfo.from_fn(_sum_list_rewrite_2),
238
+ DEFAULT_OPTIONS,
239
+ exception_equivalence=ExceptionEquivalenceType.SAME_TYPE,
240
+ )
241
+ debug("diffs=", diffs)
242
+ assert (
243
+ len(diffs) == 1
244
+ ) # finds a counter-example, because TypeError!=CustomException
245
+ assert isinstance(diffs[0], BehaviorDiff)
246
+ assert isinstance(diffs[0].result1.error, str)
247
+ assert isinstance(diffs[0].result2.error, str)
248
+ assert diffs[0].result1.error.startswith("TypeError")
249
+ assert diffs[0].result2.error.startswith("CustomException")
250
+
251
+
252
+ def test_diff_behavior_nan() -> None:
253
+ def f(x: float):
254
+ return x
255
+
256
+ diffs = diff_behavior(
257
+ FunctionInfo.from_fn(f),
258
+ FunctionInfo.from_fn(f),
259
+ DEFAULT_OPTIONS,
260
+ )
261
+ assert diffs == []