crosshair-tool 0.0.88__cp39-cp39-win32.whl → 0.0.93__cp39-cp39-win32.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.
Potentially problematic release.
This version of crosshair-tool might be problematic. Click here for more details.
- _crosshair_tracers.cp39-win32.pyd +0 -0
- crosshair/__init__.py +1 -1
- crosshair/core.py +3 -2
- crosshair/libimpl/builtinslib.py +6 -5
- crosshair/libimpl/fractionlib_test.py +24 -1
- crosshair/patch_equivalence_test.py +2 -0
- crosshair/pathing_oracle.py +5 -6
- crosshair/statespace.py +66 -90
- crosshair/test_util.py +39 -15
- crosshair/test_util_test.py +10 -0
- crosshair/type_repo.py +2 -2
- crosshair/util.py +2 -0
- {crosshair_tool-0.0.88.dist-info → crosshair_tool-0.0.93.dist-info}/METADATA +2 -1
- {crosshair_tool-0.0.88.dist-info → crosshair_tool-0.0.93.dist-info}/RECORD +18 -18
- {crosshair_tool-0.0.88.dist-info → crosshair_tool-0.0.93.dist-info}/WHEEL +0 -0
- {crosshair_tool-0.0.88.dist-info → crosshair_tool-0.0.93.dist-info}/entry_points.txt +0 -0
- {crosshair_tool-0.0.88.dist-info → crosshair_tool-0.0.93.dist-info}/licenses/LICENSE +0 -0
- {crosshair_tool-0.0.88.dist-info → crosshair_tool-0.0.93.dist-info}/top_level.txt +0 -0
|
Binary file
|
crosshair/__init__.py
CHANGED
|
@@ -15,7 +15,7 @@ from crosshair.statespace import StateSpace
|
|
|
15
15
|
from crosshair.tracers import NoTracing, ResumedTracing
|
|
16
16
|
from crosshair.util import IgnoreAttempt, debug
|
|
17
17
|
|
|
18
|
-
__version__ = "0.0.
|
|
18
|
+
__version__ = "0.0.93" # Do not forget to update in setup.py!
|
|
19
19
|
__author__ = "Phillip Schanely"
|
|
20
20
|
__license__ = "MIT"
|
|
21
21
|
__status__ = "Alpha"
|
crosshair/core.py
CHANGED
|
@@ -329,9 +329,10 @@ def with_realized_args(fn: Callable, deep=False) -> Callable:
|
|
|
329
329
|
|
|
330
330
|
def realizer(*a, **kw):
|
|
331
331
|
with NoTracing():
|
|
332
|
-
a =
|
|
332
|
+
a = [realize_fn(arg) for arg in a]
|
|
333
333
|
kw = {k: realize_fn(v) for (k, v) in kw.items()}
|
|
334
|
-
|
|
334
|
+
# You might think we don't need tracing here, but some operations can invoke user-defined behavior:
|
|
335
|
+
return fn(*a, **kw)
|
|
335
336
|
|
|
336
337
|
functools.update_wrapper(realizer, fn)
|
|
337
338
|
return realizer
|
crosshair/libimpl/builtinslib.py
CHANGED
|
@@ -4441,10 +4441,11 @@ def _int(val: Any = 0, base=_MISSING):
|
|
|
4441
4441
|
else:
|
|
4442
4442
|
ret = (ret * base) + ch_num
|
|
4443
4443
|
return ret
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4444
|
+
elif isinstance(val, CrossHairValue):
|
|
4445
|
+
val = deep_realize(val)
|
|
4446
|
+
base = deep_realize(base)
|
|
4447
|
+
|
|
4448
|
+
return int(val) if base is _MISSING else int(val, base=base)
|
|
4448
4449
|
|
|
4449
4450
|
|
|
4450
4451
|
_FLOAT_REGEX = re.compile(
|
|
@@ -4549,7 +4550,7 @@ def _len(ls):
|
|
|
4549
4550
|
def _map(fn, *iters):
|
|
4550
4551
|
# Wrap the `map` callback in a pure Python lambda.
|
|
4551
4552
|
# This de-optimization ensures that the callback can be intercepted.
|
|
4552
|
-
return map(lambda
|
|
4553
|
+
return map(lambda *a: fn(*a), *iters)
|
|
4553
4554
|
|
|
4554
4555
|
|
|
4555
4556
|
def _memoryview(source):
|
|
@@ -5,7 +5,8 @@ from crosshair.core import deep_realize
|
|
|
5
5
|
from crosshair.core_and_libs import proxy_for_type
|
|
6
6
|
from crosshair.statespace import POST_FAIL
|
|
7
7
|
from crosshair.test_util import check_states
|
|
8
|
-
from crosshair.tracers import ResumedTracing
|
|
8
|
+
from crosshair.tracers import ResumedTracing, is_tracing
|
|
9
|
+
from crosshair.util import CrossHairInternal
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def test_fraction_realize(space):
|
|
@@ -16,6 +17,28 @@ def test_fraction_realize(space):
|
|
|
16
17
|
deep_realize(Fraction(n, d))
|
|
17
18
|
|
|
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
|
+
|
|
19
42
|
def test_fraction_copy_doesnt_realize(space):
|
|
20
43
|
n = proxy_for_type(int, "n")
|
|
21
44
|
with ResumedTracing():
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
import itertools
|
|
3
|
+
import operator
|
|
3
4
|
import re
|
|
4
5
|
import sys
|
|
5
6
|
from dataclasses import dataclass
|
|
@@ -39,6 +40,7 @@ possible_args = [
|
|
|
39
40
|
(42, int), # isinstance
|
|
40
41
|
(re.compile("(ab|a|b)"), r"\n", ""), # re methods
|
|
41
42
|
(bool, [1, 1, 0]), # itertools.takewhile and friends
|
|
43
|
+
(operator.add, [1, 0], [1, 1]), # multi-iterable map
|
|
42
44
|
([(1, 2), (3, 4)]), # key-value pairs
|
|
43
45
|
([(1, 2), ([], 4)]), # key-value pairs w/ unhashable key
|
|
44
46
|
]
|
crosshair/pathing_oracle.py
CHANGED
|
@@ -6,7 +6,7 @@ from z3 import ExprRef # type: ignore
|
|
|
6
6
|
|
|
7
7
|
from crosshair.statespace import (
|
|
8
8
|
AbstractPathingOracle,
|
|
9
|
-
|
|
9
|
+
DetachedPathNode,
|
|
10
10
|
ModelValueNode,
|
|
11
11
|
NodeLike,
|
|
12
12
|
RootNode,
|
|
@@ -117,14 +117,13 @@ class CoveragePathingOracle(AbstractPathingOracle):
|
|
|
117
117
|
for step, node in enumerate(path[:-1]):
|
|
118
118
|
if not isinstance(node, NodeLike):
|
|
119
119
|
continue
|
|
120
|
-
node = node.simplify() # type: ignore
|
|
121
120
|
if isinstance(node, WorstResultNode):
|
|
122
121
|
key = node.stacktail
|
|
123
122
|
if (key not in leading_locs) and (not isinstance(node, ModelValueNode)):
|
|
124
123
|
self.summarized_positions[key] += Counter(leading_conditions)
|
|
125
124
|
leading_locs.append(key)
|
|
126
|
-
next_node = path[step + 1]
|
|
127
|
-
if isinstance(next_node,
|
|
125
|
+
next_node = path[step + 1]
|
|
126
|
+
if isinstance(next_node, DetachedPathNode):
|
|
128
127
|
break
|
|
129
128
|
if step + 1 < len(path):
|
|
130
129
|
(is_positive, root_expr) = node.normalized_expr
|
|
@@ -133,9 +132,9 @@ class CoveragePathingOracle(AbstractPathingOracle):
|
|
|
133
132
|
if is_positive
|
|
134
133
|
else -self.internalize(root_expr)
|
|
135
134
|
)
|
|
136
|
-
if next_node == node.positive
|
|
135
|
+
if next_node == node.positive:
|
|
137
136
|
leading_conditions.append(expr_signature)
|
|
138
|
-
elif next_node == node.negative
|
|
137
|
+
elif next_node == node.negative:
|
|
139
138
|
leading_conditions.append(-expr_signature)
|
|
140
139
|
else:
|
|
141
140
|
raise CrossHairInternal(
|
crosshair/statespace.py
CHANGED
|
@@ -290,49 +290,10 @@ class NodeLike:
|
|
|
290
290
|
"""
|
|
291
291
|
raise NotImplementedError
|
|
292
292
|
|
|
293
|
-
def is_stem(self) -> bool:
|
|
294
|
-
return False
|
|
295
|
-
|
|
296
|
-
def grow_into(self, node: _N) -> _N:
|
|
297
|
-
raise NotImplementedError
|
|
298
|
-
|
|
299
|
-
def simplify(self) -> "NodeLike":
|
|
300
|
-
return self
|
|
301
|
-
|
|
302
293
|
def stats(self) -> StateSpaceCounter:
|
|
303
294
|
raise NotImplementedError
|
|
304
295
|
|
|
305
296
|
|
|
306
|
-
class NodeStem(NodeLike):
|
|
307
|
-
evolution: Optional["SearchTreeNode"] = None
|
|
308
|
-
|
|
309
|
-
def is_exhausted(self) -> bool:
|
|
310
|
-
return False if self.evolution is None else self.evolution.is_exhausted()
|
|
311
|
-
|
|
312
|
-
def get_result(self) -> CallAnalysis:
|
|
313
|
-
return (
|
|
314
|
-
CallAnalysis(VerificationStatus.UNKNOWN)
|
|
315
|
-
if self.evolution is None
|
|
316
|
-
else self.evolution.get_result()
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
def is_stem(self) -> bool:
|
|
320
|
-
return self.evolution is None or self.evolution.is_stem()
|
|
321
|
-
|
|
322
|
-
def grow_into(self, node: _N) -> _N:
|
|
323
|
-
self.evolution = node
|
|
324
|
-
return node
|
|
325
|
-
|
|
326
|
-
def simplify(self):
|
|
327
|
-
return self if self.evolution is None else self.evolution
|
|
328
|
-
|
|
329
|
-
def stats(self) -> StateSpaceCounter:
|
|
330
|
-
return StateSpaceCounter() if self.evolution is None else self.evolution.stats()
|
|
331
|
-
|
|
332
|
-
def __repr__(self) -> str:
|
|
333
|
-
return "NodeStem()"
|
|
334
|
-
|
|
335
|
-
|
|
336
297
|
class SearchTreeNode(NodeLike):
|
|
337
298
|
"""A node in the execution path tree."""
|
|
338
299
|
|
|
@@ -364,6 +325,27 @@ class SearchTreeNode(NodeLike):
|
|
|
364
325
|
raise NotImplementedError
|
|
365
326
|
|
|
366
327
|
|
|
328
|
+
class NodeStem(NodeLike):
|
|
329
|
+
def __init__(self, parent: SearchTreeNode, parent_attr_name: str):
|
|
330
|
+
self.parent = parent
|
|
331
|
+
self.parent_attr_name = parent_attr_name
|
|
332
|
+
|
|
333
|
+
def grow(self, node: SearchTreeNode):
|
|
334
|
+
setattr(self.parent, self.parent_attr_name, node)
|
|
335
|
+
|
|
336
|
+
def is_exhausted(self) -> bool:
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
def get_result(self) -> CallAnalysis:
|
|
340
|
+
return CallAnalysis(VerificationStatus.UNKNOWN)
|
|
341
|
+
|
|
342
|
+
def stats(self) -> StateSpaceCounter:
|
|
343
|
+
return StateSpaceCounter()
|
|
344
|
+
|
|
345
|
+
def __repr__(self) -> str:
|
|
346
|
+
return "NodeStem()"
|
|
347
|
+
|
|
348
|
+
|
|
367
349
|
def solver_is_sat(solver, *exprs) -> bool:
|
|
368
350
|
ret = solver.check(*exprs)
|
|
369
351
|
if ret == z3.unknown:
|
|
@@ -411,7 +393,7 @@ class SinglePathNode(SearchTreeNode):
|
|
|
411
393
|
|
|
412
394
|
def __init__(self, decision: bool):
|
|
413
395
|
self.decision = decision
|
|
414
|
-
self.child = NodeStem()
|
|
396
|
+
self.child = NodeStem(self, "child")
|
|
415
397
|
self._random = newrandom()
|
|
416
398
|
|
|
417
399
|
def choose(
|
|
@@ -420,7 +402,7 @@ class SinglePathNode(SearchTreeNode):
|
|
|
420
402
|
return (self.decision, 1.0, self.child)
|
|
421
403
|
|
|
422
404
|
def compute_result(self, leaf_analysis: CallAnalysis) -> Tuple[CallAnalysis, bool]:
|
|
423
|
-
self.child
|
|
405
|
+
assert isinstance(self.child, SearchTreeNode)
|
|
424
406
|
return (self.child.get_result(), self.child.is_exhausted())
|
|
425
407
|
|
|
426
408
|
def stats(self) -> StateSpaceCounter:
|
|
@@ -449,7 +431,7 @@ class RootNode(SinglePathNode):
|
|
|
449
431
|
self.iteration = 0
|
|
450
432
|
|
|
451
433
|
|
|
452
|
-
class
|
|
434
|
+
class DetachedPathNode(SinglePathNode):
|
|
453
435
|
def __init__(self):
|
|
454
436
|
super().__init__(True)
|
|
455
437
|
# Seems like `exhausted` should be True, but we set to False until we can
|
|
@@ -459,7 +441,6 @@ class DeatchedPathNode(SinglePathNode):
|
|
|
459
441
|
self._stats = None
|
|
460
442
|
|
|
461
443
|
def compute_result(self, leaf_analysis: CallAnalysis) -> Tuple[CallAnalysis, bool]:
|
|
462
|
-
self.child = self.child.simplify()
|
|
463
444
|
return (leaf_analysis, True)
|
|
464
445
|
|
|
465
446
|
def stats(self) -> StateSpaceCounter:
|
|
@@ -494,8 +475,8 @@ class RandomizedBinaryPathNode(BinaryPathNode):
|
|
|
494
475
|
def __init__(self, rand: random.Random):
|
|
495
476
|
super().__init__()
|
|
496
477
|
self._random = rand
|
|
497
|
-
self.positive = NodeStem()
|
|
498
|
-
self.negative = NodeStem()
|
|
478
|
+
self.positive = NodeStem(self, "positive")
|
|
479
|
+
self.negative = NodeStem(self, "negative")
|
|
499
480
|
|
|
500
481
|
def probability_true(
|
|
501
482
|
self, space: "StateSpace", requested_probability: Optional[float] = None
|
|
@@ -520,10 +501,6 @@ class RandomizedBinaryPathNode(BinaryPathNode):
|
|
|
520
501
|
else:
|
|
521
502
|
return (positive_ok, 1.0, self.positive if positive_ok else self.negative)
|
|
522
503
|
|
|
523
|
-
def _simplify(self) -> None:
|
|
524
|
-
self.positive = self.positive.simplify()
|
|
525
|
-
self.negative = self.negative.simplify()
|
|
526
|
-
|
|
527
504
|
|
|
528
505
|
class ParallelNode(RandomizedBinaryPathNode):
|
|
529
506
|
"""Choose either path; the first complete result will be used."""
|
|
@@ -537,7 +514,6 @@ class ParallelNode(RandomizedBinaryPathNode):
|
|
|
537
514
|
return f"ParallelNode(false_pct={self._false_probability}, {self._desc})"
|
|
538
515
|
|
|
539
516
|
def compute_result(self, leaf_analysis: CallAnalysis) -> Tuple[CallAnalysis, bool]:
|
|
540
|
-
self._simplify()
|
|
541
517
|
positive, negative = self.positive, self.negative
|
|
542
518
|
pos_exhausted = positive.is_exhausted()
|
|
543
519
|
neg_exhausted = negative.is_exhausted()
|
|
@@ -655,7 +631,6 @@ class WorstResultNode(RandomizedBinaryPathNode):
|
|
|
655
631
|
)
|
|
656
632
|
|
|
657
633
|
def compute_result(self, leaf_analysis: CallAnalysis) -> Tuple[CallAnalysis, bool]:
|
|
658
|
-
self._simplify()
|
|
659
634
|
positive, negative = self.positive, self.negative
|
|
660
635
|
exhausted = self._is_exhausted()
|
|
661
636
|
if node_status(positive) == VerificationStatus.REFUTED or (
|
|
@@ -697,7 +672,6 @@ class ModelValueNode(WorstResultNode):
|
|
|
697
672
|
|
|
698
673
|
def debug_path_tree(node, highlights, prefix="") -> List[str]:
|
|
699
674
|
highlighted = node in highlights
|
|
700
|
-
node = node.simplify()
|
|
701
675
|
highlighted |= node in highlights
|
|
702
676
|
if isinstance(node, BinaryPathNode):
|
|
703
677
|
if isinstance(node, WorstResultNode) and node.forced_path is not None:
|
|
@@ -806,23 +780,27 @@ class StateSpace:
|
|
|
806
780
|
return value # type: ignore
|
|
807
781
|
|
|
808
782
|
def stats_lookahead(self) -> Tuple[StateSpaceCounter, StateSpaceCounter]:
|
|
809
|
-
node = self._search_position
|
|
810
|
-
if node
|
|
783
|
+
node = self._search_position
|
|
784
|
+
if isinstance(node, NodeStem):
|
|
811
785
|
return (StateSpaceCounter(), StateSpaceCounter())
|
|
812
|
-
assert isinstance(
|
|
813
|
-
node, BinaryPathNode
|
|
814
|
-
), f"node {node} {node.is_stem()} is not a binarypathnode"
|
|
786
|
+
assert isinstance(node, BinaryPathNode), f"node {node} is not a binarypathnode"
|
|
815
787
|
return node.stats_lookahead()
|
|
816
788
|
|
|
789
|
+
def grow_into(self, node: _N) -> _N:
|
|
790
|
+
assert isinstance(self._search_position, NodeStem)
|
|
791
|
+
self._search_position.grow(node)
|
|
792
|
+
node.iteration = self._root.iteration
|
|
793
|
+
self._search_position = node
|
|
794
|
+
return node
|
|
795
|
+
|
|
817
796
|
def fork_parallel(self, false_probability: float, desc: str = "") -> bool:
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
)
|
|
797
|
+
node = self._search_position
|
|
798
|
+
if isinstance(node, NodeStem):
|
|
799
|
+
node = self.grow_into(ParallelNode(self._random, false_probability, desc))
|
|
800
|
+
node.stacktail = self.gen_stack_descriptions()
|
|
822
801
|
assert isinstance(node, ParallelNode)
|
|
823
802
|
self._search_position = node
|
|
824
803
|
else:
|
|
825
|
-
node = self._search_position.simplify()
|
|
826
804
|
if not isinstance(node, ParallelNode):
|
|
827
805
|
self.raise_not_deterministic(
|
|
828
806
|
node, "Wrong node type (expected ParallelNode)"
|
|
@@ -878,33 +856,34 @@ class StateSpace:
|
|
|
878
856
|
# NOTE: format_stack() is more human readable, but it pulls source file contents,
|
|
879
857
|
# so it is (1) slow, and (2) unstable when source code changes while we are checking.
|
|
880
858
|
stacktail = self.gen_stack_descriptions()
|
|
881
|
-
if self._search_position
|
|
882
|
-
|
|
883
|
-
# to think about how mutating an existing path branch would work:
|
|
884
|
-
self.check_timeout()
|
|
885
|
-
node = self._search_position.grow_into(
|
|
886
|
-
WorstResultNode(self._random, expr, self.solver)
|
|
887
|
-
)
|
|
888
|
-
node.iteration = self._root.iteration
|
|
889
|
-
node.stacktail = stacktail
|
|
890
|
-
else:
|
|
891
|
-
node = self._search_position.simplify() # type: ignore
|
|
859
|
+
if isinstance(self._search_position, SearchTreeNode):
|
|
860
|
+
node = self._search_position
|
|
892
861
|
not_deterministic_reason = (
|
|
893
862
|
(
|
|
894
863
|
(not isinstance(node, WorstResultNode))
|
|
895
|
-
and "Wrong node type (expected WorstResultNode)"
|
|
864
|
+
and f"Wrong node type (is {name_of_type(type(node))}, expected WorstResultNode)"
|
|
896
865
|
)
|
|
897
866
|
# TODO: Not clear whether we want this stack trace check.
|
|
898
867
|
# A stack change usually indicates a serious problem, but not 100% of the time.
|
|
899
868
|
# Keeping it would mean that we fail earlier.
|
|
900
869
|
# But also see https://github.com/HypothesisWorks/hypothesis/pull/4034#issuecomment-2606415404
|
|
901
870
|
# or (node.stacktail != stacktail and "Stack trace changed")
|
|
902
|
-
or (
|
|
871
|
+
or (
|
|
872
|
+
(hasattr(node, "expr") and (not z3.eq(node.expr, expr)))
|
|
873
|
+
and "SMT expression changed"
|
|
874
|
+
)
|
|
903
875
|
)
|
|
904
876
|
if not_deterministic_reason:
|
|
905
877
|
self.raise_not_deterministic(
|
|
906
878
|
node, not_deterministic_reason, expr=expr, stacktail=stacktail
|
|
907
879
|
)
|
|
880
|
+
else:
|
|
881
|
+
# We only allow time outs at stems - that's because we don't want
|
|
882
|
+
# to think about how mutating an existing path branch would work:
|
|
883
|
+
self.check_timeout()
|
|
884
|
+
node = self.grow_into(WorstResultNode(self._random, expr, self.solver))
|
|
885
|
+
node.stacktail = stacktail
|
|
886
|
+
|
|
908
887
|
self._search_position = node
|
|
909
888
|
choose_true, chosen_probability, stem = node.choose(
|
|
910
889
|
self, probability_true=probability_true
|
|
@@ -937,10 +916,10 @@ class StateSpace:
|
|
|
937
916
|
currently_handling: Optional[BaseException] = None,
|
|
938
917
|
) -> NoReturn:
|
|
939
918
|
lines = ["*** Begin Not Deterministic Debug ***"]
|
|
940
|
-
if
|
|
941
|
-
lines.append(f"Previous iteration: {node.iteration}")
|
|
919
|
+
if getattr(node, "iteration", None) is not None:
|
|
920
|
+
lines.append(f"Previous iteration: {node.iteration}") # type: ignore
|
|
942
921
|
if hasattr(node, "expr"):
|
|
943
|
-
lines.append(f"Previous SMT expression: {node.expr}")
|
|
922
|
+
lines.append(f"Previous SMT expression: {node.expr}") # type: ignore
|
|
944
923
|
if expr is not None:
|
|
945
924
|
lines.append(f"Current SMT expression: {expr}")
|
|
946
925
|
if not stacktail:
|
|
@@ -966,11 +945,11 @@ class StateSpace:
|
|
|
966
945
|
def find_model_value(self, expr: z3.ExprRef) -> Any:
|
|
967
946
|
with NoTracing():
|
|
968
947
|
while True:
|
|
969
|
-
if self._search_position
|
|
970
|
-
self._search_position = self.
|
|
948
|
+
if isinstance(self._search_position, NodeStem):
|
|
949
|
+
self._search_position = self.grow_into(
|
|
971
950
|
ModelValueNode(self._random, expr, self.solver)
|
|
972
951
|
)
|
|
973
|
-
node = self._search_position
|
|
952
|
+
node = self._search_position
|
|
974
953
|
if isinstance(node, SearchLeaf):
|
|
975
954
|
raise CrossHairInternal(
|
|
976
955
|
f"Cannot use symbolics; path is already terminated"
|
|
@@ -1109,14 +1088,14 @@ class StateSpace:
|
|
|
1109
1088
|
if not prefer_true(check_ret):
|
|
1110
1089
|
raise IgnoreAttempt("deferred assumption failed: " + description)
|
|
1111
1090
|
self.is_detached = True
|
|
1112
|
-
if not self._search_position
|
|
1091
|
+
if not isinstance(self._search_position, NodeStem):
|
|
1113
1092
|
self.raise_not_deterministic(
|
|
1114
1093
|
self._search_position,
|
|
1115
|
-
f"Expect to detach path at a stem node, not at this node: {self._search_position
|
|
1094
|
+
f"Expect to detach path at a stem node, not at this node: {self._search_position}",
|
|
1116
1095
|
currently_handling=currently_handling,
|
|
1117
1096
|
)
|
|
1118
|
-
node = self.
|
|
1119
|
-
assert node.child
|
|
1097
|
+
node = self.grow_into(DetachedPathNode())
|
|
1098
|
+
assert isinstance(node.child, NodeStem)
|
|
1120
1099
|
self.choices_made.append(node)
|
|
1121
1100
|
self._search_position = node.child
|
|
1122
1101
|
debug("Detached from search tree")
|
|
@@ -1130,12 +1109,9 @@ class StateSpace:
|
|
|
1130
1109
|
self, analysis: CallAnalysis
|
|
1131
1110
|
) -> Tuple[Optional[CallAnalysis], bool]:
|
|
1132
1111
|
# In some cases, we might ignore an attempt while not at a leaf.
|
|
1133
|
-
if self._search_position
|
|
1134
|
-
self._search_position = self.
|
|
1135
|
-
SearchLeaf(analysis)
|
|
1136
|
-
)
|
|
1112
|
+
if isinstance(self._search_position, NodeStem):
|
|
1113
|
+
self._search_position = self.grow_into(SearchLeaf(analysis))
|
|
1137
1114
|
else:
|
|
1138
|
-
self._search_position = self._search_position.simplify()
|
|
1139
1115
|
assert isinstance(self._search_position, SearchTreeNode)
|
|
1140
1116
|
self._search_position.exhausted = True
|
|
1141
1117
|
self._search_position.result = analysis
|
crosshair/test_util.py
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import pathlib
|
|
2
2
|
import sys
|
|
3
|
-
from collections.abc import Container
|
|
4
3
|
from copy import deepcopy
|
|
5
4
|
from dataclasses import dataclass, replace
|
|
6
|
-
from
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from math import isnan
|
|
7
|
+
from numbers import Real
|
|
8
|
+
from typing import (
|
|
9
|
+
Callable,
|
|
10
|
+
Collection,
|
|
11
|
+
Iterable,
|
|
12
|
+
List,
|
|
13
|
+
Mapping,
|
|
14
|
+
Optional,
|
|
15
|
+
Sequence,
|
|
16
|
+
Set,
|
|
17
|
+
Tuple,
|
|
18
|
+
)
|
|
7
19
|
|
|
8
20
|
from crosshair.core import (
|
|
9
21
|
AnalysisMessage,
|
|
@@ -29,6 +41,13 @@ from crosshair.util import (
|
|
|
29
41
|
ComparableLists = Tuple[List, List]
|
|
30
42
|
|
|
31
43
|
|
|
44
|
+
class _Missing:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_MISSING = _Missing()
|
|
49
|
+
|
|
50
|
+
|
|
32
51
|
def simplefs(path: pathlib.Path, files: dict) -> None:
|
|
33
52
|
for name, contents in files.items():
|
|
34
53
|
subpath = path / name
|
|
@@ -123,34 +142,39 @@ def check_messages(checkables: Iterable[Checkable], **kw) -> ComparableLists:
|
|
|
123
142
|
return (msgs, [AnalysisMessage(**kw)])
|
|
124
143
|
|
|
125
144
|
|
|
126
|
-
|
|
145
|
+
_NAN_ABLE = (Decimal, Real)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def flexible_equal(a: object, b: object) -> bool:
|
|
127
149
|
if a is b:
|
|
128
150
|
return True
|
|
129
151
|
if type(a) is type(b) and type(a).__eq__ is object.__eq__:
|
|
130
152
|
# If types match and it uses identity-equals, we can't do much. Assume equal.
|
|
131
153
|
return True
|
|
132
|
-
if a
|
|
154
|
+
if isinstance(a, _NAN_ABLE) and isinstance(b, _NAN_ABLE) and isnan(a) and isnan(b):
|
|
133
155
|
return True
|
|
134
156
|
if (
|
|
135
157
|
is_iterable(a)
|
|
136
|
-
and not isinstance(a,
|
|
158
|
+
and not isinstance(a, Collection)
|
|
137
159
|
and is_iterable(b)
|
|
138
|
-
and not isinstance(b,
|
|
160
|
+
and not isinstance(b, Collection)
|
|
139
161
|
): # unsized iterables compare by contents
|
|
140
|
-
a, b = list(a), list(b)
|
|
141
|
-
if
|
|
162
|
+
a, b = list(a), list(b) # type: ignore
|
|
163
|
+
if (
|
|
164
|
+
type(a) == type(b)
|
|
165
|
+
and isinstance(a, Collection)
|
|
166
|
+
and not isinstance(a, (str, bytes, Set))
|
|
167
|
+
):
|
|
142
168
|
# Recursively apply flexible_equal for most containers:
|
|
169
|
+
if len(a) != len(b): # type: ignore
|
|
170
|
+
return False
|
|
143
171
|
if isinstance(a, Mapping):
|
|
144
|
-
if len(a) != len(b):
|
|
145
|
-
return False
|
|
146
172
|
for k, v in a.items():
|
|
147
|
-
if not flexible_equal(v, b
|
|
173
|
+
if not flexible_equal(v, b.get(k, _MISSING)): # type: ignore
|
|
148
174
|
return False
|
|
149
175
|
return True
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return False
|
|
153
|
-
return all(flexible_equal(ai, bi) for ai, bi in zip(a, b))
|
|
176
|
+
else:
|
|
177
|
+
return all(flexible_equal(ai, bi) for ai, bi in zip(a, b)) # type: ignore
|
|
154
178
|
|
|
155
179
|
return a == b
|
|
156
180
|
|
crosshair/test_util_test.py
CHANGED
|
@@ -14,3 +14,13 @@ def test_flexible_equal():
|
|
|
14
14
|
assert flexible_equal(gen(), iter([11, 22]))
|
|
15
15
|
assert not flexible_equal(gen(), iter([11, 22, 33]))
|
|
16
16
|
assert not flexible_equal(gen(), iter([11]))
|
|
17
|
+
|
|
18
|
+
ordered_set_1 = {10_000, 20_000} | {30_000}
|
|
19
|
+
ordered_set_2 = {30_000, 20_000} | {10_000}
|
|
20
|
+
assert list(ordered_set_1) != list(ordered_set_2) # (different orderings)
|
|
21
|
+
assert flexible_equal(ordered_set_1, ordered_set_2)
|
|
22
|
+
|
|
23
|
+
ordered_dict_1 = {1: 2, 3: 4}
|
|
24
|
+
ordered_dict_2 = {3: 4, 1: 2}
|
|
25
|
+
assert list(ordered_dict_1.items()) != list(ordered_dict_2.items())
|
|
26
|
+
assert flexible_equal(ordered_dict_1, ordered_dict_2)
|
crosshair/type_repo.py
CHANGED
|
@@ -20,6 +20,8 @@ _IGNORED_MODULE_ROOTS = {
|
|
|
20
20
|
"pkg_resources",
|
|
21
21
|
"pytest",
|
|
22
22
|
"py", # (part of pytest)
|
|
23
|
+
# Disabled because the import attempt can cause problems:
|
|
24
|
+
"numpy", # importing numpy/testing/_private/utils.py attempts a subprocess call
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
|
|
@@ -59,8 +61,6 @@ def get_subclass_map() -> Dict[type, List[type]]:
|
|
|
59
61
|
if module_name.split(".", 1)[0] in _IGNORED_MODULE_ROOTS:
|
|
60
62
|
continue
|
|
61
63
|
if module is None:
|
|
62
|
-
# We set the internal _datetime module to None, ensuring that
|
|
63
|
-
# we don't load the C implementation.
|
|
64
64
|
continue
|
|
65
65
|
try:
|
|
66
66
|
module_classes = inspect.getmembers(module, inspect.isclass)
|
crosshair/util.py
CHANGED
|
@@ -54,6 +54,8 @@ from crosshair.tracers import COMPOSITE_TRACER, NoTracing, ResumedTracing, is_tr
|
|
|
54
54
|
_DEBUG_STREAM: Optional[TextIO] = None
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
# NOTE: many of these is_* functions should use a TypeGuard in 3.10 (or even TypeIs in 3.13)
|
|
58
|
+
|
|
57
59
|
if sys.version_info >= (3, 12):
|
|
58
60
|
from collections.abc import Buffer
|
|
59
61
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crosshair-tool
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.93
|
|
4
4
|
Summary: Analyze Python code for correctness using symbolic execution.
|
|
5
5
|
Home-page: https://github.com/pschanely/CrossHair
|
|
6
6
|
Author: Phillip Schanely
|
|
@@ -47,6 +47,7 @@ Requires-Dist: setuptools; extra == "dev"
|
|
|
47
47
|
Requires-Dist: sphinx>=3.4.3; extra == "dev"
|
|
48
48
|
Requires-Dist: sphinx-rtd-theme>=0.5.1; extra == "dev"
|
|
49
49
|
Requires-Dist: rst2pdf>=0.102; extra == "dev"
|
|
50
|
+
Requires-Dist: z3-solver==4.14.1.0; extra == "dev"
|
|
50
51
|
Dynamic: author
|
|
51
52
|
Dynamic: author-email
|
|
52
53
|
Dynamic: classifier
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
_crosshair_tracers.cp39-win32.pyd,sha256=
|
|
2
|
-
crosshair/__init__.py,sha256=
|
|
1
|
+
_crosshair_tracers.cp39-win32.pyd,sha256=WdlUdnnnS1Im5DEy2X6G04Ch0XOC9YvrKhESfpM2I1A,16896
|
|
2
|
+
crosshair/__init__.py,sha256=aNQs81VDq95_ZF8wWBUtdIO4BNRcGH-CkBCuKIy37dE,978
|
|
3
3
|
crosshair/__main__.py,sha256=i4pjsZlXvtieQHJTEB0sF3e40qJjLoOm1z5rm-u70Uo,260
|
|
4
4
|
crosshair/_mark_stacks.h,sha256=IImygydFK9qethf41gUZDjySHR3WLXs-YsqWCHK9UFg,29468
|
|
5
5
|
crosshair/_preliminaries_test.py,sha256=klkzKGpyyVDGctCXdvq1xyXbGa4PRnXda1jfgE-7LLY,522
|
|
@@ -16,7 +16,7 @@ crosshair/condition_parser_test.py,sha256=kosW60A4HnBBT3sOHxWCl_cixpbO1kmsjgNuNU
|
|
|
16
16
|
crosshair/conftest.py,sha256=JjUauvXnjERszuS46iskuBRHBx-0uHacwKHeQ-gEQ2c,683
|
|
17
17
|
crosshair/copyext.py,sha256=_SRnS6UrUFl2-CHl8uRIOq-Y0MKYNQTFK132S3Ru34A,5044
|
|
18
18
|
crosshair/copyext_test.py,sha256=VSIoVeqAf18g2RNuLTbyNWJiMBgzHeab7LNX1KHCWZU,2182
|
|
19
|
-
crosshair/core.py,sha256=
|
|
19
|
+
crosshair/core.py,sha256=bUkWzbyhEvk24_UJ7tYo489ccq-Q6V0Pnpj4aY165qg,65497
|
|
20
20
|
crosshair/core_and_libs.py,sha256=U9GDCBE9Ecxy7T7bmJT7IYiQTdY1C8qbcoIoMMocu5U,4112
|
|
21
21
|
crosshair/core_regestered_types_test.py,sha256=3o1GrtjCEWSW-paPp5-yVdE2K1_ZIviGCff7KVZ0VS0,2181
|
|
22
22
|
crosshair/core_test.py,sha256=bHpq_OcUsVWFWGnsCUX4AEM22vLfoCzCsPaEnsbAzYc,33490
|
|
@@ -39,12 +39,12 @@ crosshair/opcode_intercept.py,sha256=RGcwx4f5-KKjY2C3Do4mBm4S8rEpn6nWtOQvaSKQnoM
|
|
|
39
39
|
crosshair/opcode_intercept_test.py,sha256=giJ9HK0jJ4r41epJMZ4PPT6-qn8nqDjS-900ULxIlLM,9545
|
|
40
40
|
crosshair/options.py,sha256=y3lYP5AfwkHFXjF7zl0kUrTH1_C5u1DMo_yAH_mivm8,6993
|
|
41
41
|
crosshair/options_test.py,sha256=eTlaTqNb29I3ZyyV0yCq_kEeE441l4xh-jKF_W6cWqs,389
|
|
42
|
-
crosshair/patch_equivalence_test.py,sha256=
|
|
42
|
+
crosshair/patch_equivalence_test.py,sha256=2obUF4jZWlF_HBawn6uOb53XuD7qNRiX6f8Y1A5-fUI,2655
|
|
43
43
|
crosshair/path_cover.py,sha256=rHWsTLVr-LR5rEBu18xRSJbpWk3ZTl11OG_QOkgRrxI,6915
|
|
44
44
|
crosshair/path_cover_test.py,sha256=DFB_fIC2d1waNfyUAP4wVv-hdbNkHAKRxgBcftxcKLY,4227
|
|
45
45
|
crosshair/path_search.py,sha256=5gsiY41aMcTnO5XONgjsnVulWbRIw0mYcpmRYedqORo,5749
|
|
46
46
|
crosshair/path_search_test.py,sha256=xhY7qmLRaDMnueFgokJaDKOufs2fhJdqU6okM4hWgF4,1744
|
|
47
|
-
crosshair/pathing_oracle.py,sha256=
|
|
47
|
+
crosshair/pathing_oracle.py,sha256=XmCva1sjBbQfzeQZ01UozNP-Mbdak3PepaPqQPIuylM,9453
|
|
48
48
|
crosshair/pure_importer.py,sha256=yA1HkM3Jykt2w00OQALwxi5Gt_FFa-NU9JRiBcDS2DM,905
|
|
49
49
|
crosshair/pure_importer_test.py,sha256=Tp4RitGWUhtLhNxOlJkdz43WyXZLynARbCcY4lzG7Bk,437
|
|
50
50
|
crosshair/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -54,18 +54,18 @@ crosshair/simplestructs.py,sha256=DQu4LVS5KLEIH3-z_DEg0LU3VxovKE_JRjHvBdGCzb4,35
|
|
|
54
54
|
crosshair/simplestructs_test.py,sha256=XizES1bMUx3PAMsGw6zZIzhu-ZHaP1HUVorTI1w21_s,8922
|
|
55
55
|
crosshair/smtlib.py,sha256=WJnOtT4xSDsllQ7o-WEL_-eqJrujB9KMkMO9cVNa5YA,713
|
|
56
56
|
crosshair/smtlib_test.py,sha256=uR_X_32tqPjCp9gzExY_5AZIqw8wdOrIJ9vGONJmmOg,525
|
|
57
|
-
crosshair/statespace.py,sha256=
|
|
57
|
+
crosshair/statespace.py,sha256=gkbYlMuJwQU7UuzIJoZom3Hs50OMTkfdVGoenwRCDzY,42377
|
|
58
58
|
crosshair/statespace_test.py,sha256=Z9YENLVeEMEc0SvDt1gGYvNZuleu4lOMq4kC9kaagaw,2704
|
|
59
59
|
crosshair/stubs_parser.py,sha256=tJwChs8--UZjCtkshijzA-1K2tiZ2-mgpea_hJIdVJI,14592
|
|
60
60
|
crosshair/stubs_parser_test.py,sha256=eUIy8E7LoR7tmUy3dkZEOQqDWFq7hoqbIBdJ_nwkeIE,1271
|
|
61
|
-
crosshair/test_util.py,sha256=
|
|
62
|
-
crosshair/test_util_test.py,sha256=
|
|
61
|
+
crosshair/test_util.py,sha256=jtFZdNMR9cp-7sGmhDDfzeeIrWQAenyF4QRDE-kXdwA,10696
|
|
62
|
+
crosshair/test_util_test.py,sha256=6S3BfmWB4YM4z6D7rhc4cCtV7VAstN7L_STP1iT0QgU,969
|
|
63
63
|
crosshair/tracers.py,sha256=kMUixlsd5zMjfXt_eNn7HS86yIu6c5tP45Cold76yWs,17662
|
|
64
64
|
crosshair/tracers_test.py,sha256=73lIgNCVf4QhJo5uzoonH55vKWoYDffeYb8AdCZEQHw,3424
|
|
65
|
-
crosshair/type_repo.py,sha256=
|
|
65
|
+
crosshair/type_repo.py,sha256=9gmajt6r1XYm4OAObA4yYh2FsZIRDbxFVSa95CF3Hyg,4815
|
|
66
66
|
crosshair/unicode_categories.py,sha256=JWGv47BSq1Cj-uYSWBQ4kYlsIPlmcPvaUxO6YG8d9I0,334108
|
|
67
67
|
crosshair/unicode_categories_test.py,sha256=KZ2OKOSMALmFp1BQbWj1-x6Ek5Iv2byxF4ytx2mN7LU,766
|
|
68
|
-
crosshair/util.py,sha256=
|
|
68
|
+
crosshair/util.py,sha256=UlZYLXChOyUpjuMWQxr_nLDLwhOrDo0Lxkco06IG2m4,22954
|
|
69
69
|
crosshair/util_test.py,sha256=fIc3TeMbyu9nI430_gGbUrqXhPJ5wl9TxWAtIGgEL4s,4849
|
|
70
70
|
crosshair/watcher.py,sha256=A8ywSNPUY6Y4BHVO51dGM18nQcwGYx_kbBEwIOMTzF4,10355
|
|
71
71
|
crosshair/watcher_test.py,sha256=TCOdXmdGZ3NJ59X_w7SRVqiJY-BL2tUqyXK-3ql1d6s,2871
|
|
@@ -100,7 +100,7 @@ crosshair/libimpl/binascii_ch_test.py,sha256=qWAYU1Sid3muwYL8pvVoCsMQtm5t3J8jFto
|
|
|
100
100
|
crosshair/libimpl/binascii_test.py,sha256=Yg37_B-eywcLq7EbsJZR3BUYYF6A1PGxE4qsmeBMyXg,2037
|
|
101
101
|
crosshair/libimpl/binasciilib.py,sha256=kBFH5-W_q1gCTC-wusSq0cAsUsB_xOUpcW0XQbLzkSQ,5220
|
|
102
102
|
crosshair/libimpl/bisectlib_test.py,sha256=rVpcalZ4g3xr_b6z1Bv2FU1N07vPZyWhU9_CPzC8sBk,776
|
|
103
|
-
crosshair/libimpl/builtinslib.py,sha256=
|
|
103
|
+
crosshair/libimpl/builtinslib.py,sha256=oW9VY8DBOOouWmw6tjvVZp01-GFZ7aFwr_BW4EM3c7Y,176755
|
|
104
104
|
crosshair/libimpl/builtinslib_ch_test.py,sha256=Do8kWNgfosLfI3yzZvTZ3ceuKTF0axN1ckkNZyn4ozE,31685
|
|
105
105
|
crosshair/libimpl/builtinslib_test.py,sha256=yqNE0027NV7-5l9JI-YdsZ8_TPx4F6kOrYxA5Czu2XQ,94322
|
|
106
106
|
crosshair/libimpl/codecslib.py,sha256=cvpCbieFRB4BdfLqmKeJzpvm35qZ0ONqtilXPmN8OWI,2754
|
|
@@ -118,7 +118,7 @@ crosshair/libimpl/decimallib_ch_test.py,sha256=8iQpb1c0-TNqtY4EznYkgAGnBV2uOitFj
|
|
|
118
118
|
crosshair/libimpl/decimallib_test.py,sha256=gbLoEtgpkq6Xod-9Am18mTpVH1VY_cZHvktS4_z2yQ4,2314
|
|
119
119
|
crosshair/libimpl/encodings_ch_test.py,sha256=jZM2XqiGd-40kDiC1YSlDoQafRrbtx6BBBeHTIRx3TE,2596
|
|
120
120
|
crosshair/libimpl/fractionlib.py,sha256=lGqUcl5S9lPC18pKP8Ihzirmgn8hf2QNPvdZejzK8IM,474
|
|
121
|
-
crosshair/libimpl/fractionlib_test.py,sha256=
|
|
121
|
+
crosshair/libimpl/fractionlib_test.py,sha256=gl9PIZFoKyA10KMhfmA4SSK1i1y2yUO_IDvxIr-8lBc,2437
|
|
122
122
|
crosshair/libimpl/functoolslib.py,sha256=szPJ4ZB0CevfCxMBvVfD_3RX-BwDp0Sv5YchWncmnu4,811
|
|
123
123
|
crosshair/libimpl/functoolslib_test.py,sha256=OSeY-C9zYhFMfDHJ9QNaXxx6sVJ-CLOgrYIyXBl-vvk,1154
|
|
124
124
|
crosshair/libimpl/hashliblib.py,sha256=0s4yx2l-hNkqWEd1Xbmxf-2w_E3LmjHn0SShni_r_-8,1264
|
|
@@ -167,9 +167,9 @@ crosshair/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
167
167
|
crosshair/tools/check_help_in_doc.py,sha256=An-2EJ0_Xr-Szo431AoEgJafoFWwnPl8HoT4bi86dd8,8499
|
|
168
168
|
crosshair/tools/check_init_and_setup_coincide.py,sha256=f4TvPg1Q3C_BQYtGgPfY_fqxo_ZTeU7QCy7WwQjaIlA,3621
|
|
169
169
|
crosshair/tools/generate_demo_table.py,sha256=qm4ZUXjUBKkVZ3bVVK9h6dJMDKYme8zMl-7at_aG7b0,3958
|
|
170
|
-
crosshair_tool-0.0.
|
|
171
|
-
crosshair_tool-0.0.
|
|
172
|
-
crosshair_tool-0.0.
|
|
173
|
-
crosshair_tool-0.0.
|
|
174
|
-
crosshair_tool-0.0.
|
|
175
|
-
crosshair_tool-0.0.
|
|
170
|
+
crosshair_tool-0.0.93.dist-info/licenses/LICENSE,sha256=qQLSJN48eqvalILMr3uzkg0p74FtK7WSwkux-0twy-s,4552
|
|
171
|
+
crosshair_tool-0.0.93.dist-info/METADATA,sha256=wU-Na0XKBWSJJwBqDh4EsyZzqsW21lerfZqHpKXYIs8,6923
|
|
172
|
+
crosshair_tool-0.0.93.dist-info/WHEEL,sha256=Q3uEVTFw-CqGed7ywmQZcdvBC5FiRV941NvAhTjjkOQ,95
|
|
173
|
+
crosshair_tool-0.0.93.dist-info/entry_points.txt,sha256=u5FIPVn1jqn4Kzg5K_iNnbP6L4hQw5FWjQ0UMezG2VE,96
|
|
174
|
+
crosshair_tool-0.0.93.dist-info/top_level.txt,sha256=2jLWtM-BWg_ZYNbNfrcds0HFZD62a6J7ZIbcgcQrRk4,29
|
|
175
|
+
crosshair_tool-0.0.93.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|