Skip to content

Commit 2852bc2

Browse files
authored
Merge pull request #4217 from tybug/short-collection
Add short-circuit for trivial collections
2 parents 0e905dc + 4b20720 commit 2852bc2

File tree

8 files changed

+44
-14
lines changed

8 files changed

+44
-14
lines changed

hypothesis-python/RELEASE.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
RELEASE_TYPE: patch
2+
3+
This patch improves shrinking involving long strings or byte sequences whose value is not relevant to the failure.

hypothesis-python/src/hypothesis/internal/conjecture/choice.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,14 @@ def collection_index(choice, *, min_size, alphabet_size, to_order=identity):
6969
# We then add each element c to the index, starting from the end (so "ab" is
7070
# simpler than "ba"). Each loop takes c at position i in the sequence and
7171
# computes the number of sequences of size i which come before it in the ordering.
72-
for i, c in enumerate(reversed(choice)):
73-
index += (alphabet_size**i) * to_order(c)
72+
73+
# this running_exp computation is equivalent to doing
74+
# index += (alphabet_size**i) * n
75+
# but reuses intermediate exponentiation steps for efficiency.
76+
running_exp = 1
77+
for c in reversed(choice):
78+
index += running_exp * to_order(c)
79+
running_exp *= alphabet_size
7480
return index
7581

7682

hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1480,12 +1480,14 @@ def minimize_nodes(self, nodes):
14801480
Bytes.shrink(
14811481
value,
14821482
lambda val: self.try_shrinking_nodes(nodes, val),
1483+
min_size=kwargs["min_size"],
14831484
)
14841485
elif ir_type == "string":
14851486
String.shrink(
14861487
value,
14871488
lambda val: self.try_shrinking_nodes(nodes, val),
14881489
intervals=kwargs["intervals"],
1490+
min_size=kwargs["min_size"],
14891491
)
14901492
else:
14911493
raise NotImplementedError

hypothesis-python/src/hypothesis/internal/conjecture/shrinking/collection.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,23 @@
1414

1515

1616
class Collection(Shrinker):
17-
def setup(self, *, ElementShrinker, to_order=identity, from_order=identity):
17+
def setup(
18+
self, *, ElementShrinker, to_order=identity, from_order=identity, min_size
19+
):
1820
self.ElementShrinker = ElementShrinker
1921
self.to_order = to_order
2022
self.from_order = from_order
23+
self.min_size = min_size
2124

2225
def make_immutable(self, value):
2326
return tuple(value)
2427

28+
def short_circuit(self):
29+
zero = self.from_order(0)
30+
success = self.consider([zero] * len(self.current))
31+
# we could still simplify by deleting elements (unless we're minimal size).
32+
return success and len(self.current) == self.min_size
33+
2534
def left_is_better(self, left, right):
2635
if len(left) < len(right):
2736
return True

hypothesis-python/src/hypothesis/internal/conjecture/shrinking/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def short_circuit(self):
160160
If this returns True, the ``run`` method will terminate early
161161
without doing any more work.
162162
"""
163-
return False
163+
return False # pragma: no cover
164164

165165
def left_is_better(self, left, right):
166166
"""Returns True if the left is strictly simpler than the right

hypothesis-python/tests/conjecture/test_engine.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -201,13 +201,6 @@ def nodes(data):
201201

202202

203203
def test_draw_to_overrun(monkeypatch):
204-
# TODO_BETTER_SHRINK: sometimes we can get unlucky and fail to shrink the
205-
# initial size draw d to 2 before shrinking the 128 * d bytes, but I'm not
206-
# sure why.
207-
#
208-
# If we do get unlucky in such a way then we need more than 500 shrinks to finish.
209-
monkeypatch.setattr(engine_module, "MAX_SHRINKS", 1000)
210-
211204
@run_to_nodes
212205
def nodes(data):
213206
d = (data.draw_bytes(1, 1)[0] - 8) & 0xFF

hypothesis-python/tests/conjecture/test_minimizer.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ def test_can_sort_bytes_by_reordering_partially_not_cross_stationary_element():
7272
],
7373
)
7474
def test_shrink_strings(initial, predicate, intervals, expected):
75-
assert String.shrink(initial, predicate, intervals=intervals) == tuple(expected)
75+
assert String.shrink(
76+
initial, predicate, intervals=intervals, min_size=len(expected)
77+
) == tuple(expected)
7678

7779

7880
@pytest.mark.parametrize(
@@ -85,9 +87,11 @@ def test_shrink_strings(initial, predicate, intervals, expected):
8587
],
8688
)
8789
def test_shrink_bytes(initial, predicate, expected):
88-
assert bytes(Bytes.shrink(initial, predicate)) == expected
90+
assert bytes(Bytes.shrink(initial, predicate, min_size=len(expected))) == expected
8991

9092

9193
def test_collection_left_is_better():
92-
shrinker = Collection([1, 2, 3], lambda v: True, ElementShrinker=Integer)
94+
shrinker = Collection(
95+
[1, 2, 3], lambda v: True, ElementShrinker=Integer, min_size=3
96+
)
9397
assert not shrinker.left_is_better([1, 2, 3], [1, 2, 3])

hypothesis-python/tests/conjecture/test_shrinker.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,19 @@ def shrinker(data: ConjectureData):
520520
assert shrinker.choices == (15, 10)
521521

522522

523+
@pytest.mark.parametrize("n", [10, 50, 100, 200])
524+
def test_can_quickly_shrink_to_trivial_collection(n):
525+
@shrinking_from(ir(b"\x01" * n))
526+
def shrinker(data: ConjectureData):
527+
b = data.draw_bytes()
528+
if len(b) >= n:
529+
data.mark_interesting()
530+
531+
shrinker.fixate_shrink_passes(["minimize_individual_nodes"])
532+
assert shrinker.choices == (b"\x00" * n,)
533+
assert shrinker.calls < 10
534+
535+
523536
def test_alternative_shrinking_will_lower_to_alternate_value():
524537
# We want to reject the first integer value we see when shrinking
525538
# this alternative, because it will be the result of transmuting the

0 commit comments

Comments
 (0)