Skip to content

Commit 92af9b8

Browse files
committedAug 4, 2020
Allow simultaneously lowering two nearby blocks

File tree

4 files changed

+106
-1
lines changed

4 files changed

+106
-1
lines changed
 

‎hypothesis-python/RELEASE.rst

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
RELEASE_TYPE: patch
2+
3+
This release improves the quality of shrunk test cases in some special cases.
4+
Specifically, it should get shrinking unstuck in some scenarios which require
5+
simultaneously changing two parts of the generated test case.

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

+35-1
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,7 @@ def greedy_shrink(self):
508508
"minimize_individual_blocks",
509509
block_program("--X"),
510510
"redistribute_block_pairs",
511+
"lower_blocks_together",
511512
]
512513
)
513514

@@ -1222,14 +1223,47 @@ def boost(k):
12221223
attempt[block.start : block.end] = int_to_bytes(m - k, block.length)
12231224
try:
12241225
attempt[next_block.start : next_block.end] = int_to_bytes(
1225-
n + k, block.length
1226+
n + k, next_block.length
12261227
)
12271228
except OverflowError:
12281229
return False
12291230
return self.consider_new_buffer(attempt)
12301231

12311232
find_integer(boost)
12321233

1234+
@defines_shrink_pass()
1235+
def lower_blocks_together(self, chooser):
1236+
block = chooser.choose(self.blocks, lambda b: not b.all_zero)
1237+
1238+
# Choose the next block to be up to eight blocks onwards. We don't
1239+
# want to go too far (to avoid quadratic time) but it's worth a
1240+
# reasonable amount of lookahead, especially as we expect most
1241+
# blocks are zero by this point anyway.
1242+
next_block = self.blocks[
1243+
chooser.choose(
1244+
range(block.index + 1, min(len(self.blocks), block.index + 9)),
1245+
lambda j: not self.blocks[j].all_zero,
1246+
)
1247+
]
1248+
1249+
buffer = self.buffer
1250+
1251+
m = int_from_bytes(buffer[block.start : block.end])
1252+
n = int_from_bytes(buffer[next_block.start : next_block.end])
1253+
1254+
def lower(k):
1255+
if k > min(m, n):
1256+
return False
1257+
attempt = bytearray(buffer)
1258+
attempt[block.start : block.end] = int_to_bytes(m - k, block.length)
1259+
attempt[next_block.start : next_block.end] = int_to_bytes(
1260+
n - k, next_block.length
1261+
)
1262+
assert len(attempt) == len(buffer)
1263+
return self.consider_new_buffer(attempt)
1264+
1265+
find_integer(lower)
1266+
12331267
@defines_shrink_pass()
12341268
def minimize_individual_blocks(self, chooser):
12351269
"""Attempt to minimize each block in sequence.

‎hypothesis-python/tests/conjecture/test_shrinker.py

+20
Original file line numberDiff line numberDiff line change
@@ -616,3 +616,23 @@ def shrinker(data):
616616
shrinker.fixate_shrink_passes(passes)
617617

618618
assert shrinker.shrink_pass(passes[-1]).calls > 0
619+
620+
621+
@pytest.mark.parametrize("n_gap", [0, 1, 2, 3])
622+
def test_can_simultaneously_lower_non_duplicated_nearby_blocks(n_gap):
623+
@shrinking_from([1, 1] + [0] * n_gap + [0, 2])
624+
def shrinker(data):
625+
# Block off lowering the whole buffer
626+
if data.draw_bits(1) == 0:
627+
data.mark_invalid()
628+
m = data.draw_bits(8)
629+
for _ in range(n_gap):
630+
data.draw_bits(8)
631+
n = data.draw_bits(16)
632+
633+
if n == m + 1:
634+
data.mark_interesting()
635+
636+
shrinker.fixate_shrink_passes(["lower_blocks_together"])
637+
638+
assert list(shrinker.buffer) == [1, 0] + [0] * n_gap + [0, 1]

‎hypothesis-python/tests/quality/test_shrink_quality.py

+46
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import pytest
2121

22+
import hypothesis.strategies as st
2223
from hypothesis import assume, settings
2324
from hypothesis.strategies import (
2425
booleans,
@@ -337,3 +338,48 @@ def test_sum_of_pair():
337338
assert minimal(
338339
tuples(integers(0, 1000), integers(0, 1000)), lambda x: sum(x) > 1000
339340
) == (1, 1000)
341+
342+
343+
def test_calculator_benchmark():
344+
"""This test comes from
345+
https://github.com/jlink/shrinking-challenge/blob/main/challenges/calculator.md,
346+
which is originally from Pike, Lee. "SmartCheck: automatic and efficient
347+
counterexample reduction and generalization."
348+
Proceedings of the 2014 ACM SIGPLAN symposium on Haskell. 2014.
349+
"""
350+
351+
expression = st.deferred(
352+
lambda: st.one_of(
353+
st.integers(),
354+
st.tuples(st.just("+"), expression, expression),
355+
st.tuples(st.just("/"), expression, expression),
356+
)
357+
)
358+
359+
def div_subterms(e):
360+
if isinstance(e, int):
361+
return True
362+
if e[0] == "/" and e[-1] == 0:
363+
return False
364+
return div_subterms(e[1]) and div_subterms(e[2])
365+
366+
def evaluate(e):
367+
if isinstance(e, int):
368+
return e
369+
elif e[0] == "+":
370+
return evaluate(e[1]) + evaluate(e[2])
371+
else:
372+
assert e[0] == "/"
373+
return evaluate(e[1]) // evaluate(e[2])
374+
375+
def is_failing(e):
376+
assume(div_subterms(e))
377+
try:
378+
evaluate(e)
379+
return False
380+
except ZeroDivisionError:
381+
return True
382+
383+
x = minimal(expression, is_failing)
384+
385+
assert x == ("/", 0, ("+", 0, 0))

0 commit comments

Comments
 (0)
Please sign in to comment.