Skip to content

Commit

Permalink
Merge pull request #1608 from Zalathar/length-shrinker
Browse files Browse the repository at this point in the history
Explain how Length.run_step works
  • Loading branch information
Zac-HD committed Oct 10, 2018
2 parents 49b536e + acad9ca commit 1652781
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 6 deletions.
4 changes: 4 additions & 0 deletions hypothesis-python/RELEASE.rst
@@ -0,0 +1,4 @@
RELEASE_TYPE: patch

This patch adds more internal comments to the core engine's sequence-length
shrinker. There should be no user-visible change.
Expand Up @@ -53,13 +53,51 @@ def left_is_better(self, left, right):
return len(left) < len(right)

def run_step(self):
j = 0
while j < len(self.current):
i = len(self.current) - 1 - j
# Try to delete as many elements as possible from the sequence, in
# (roughly) one pass, from right to left.

# Starting from the end of the sequence, we try to delete as many
# consecutive elements as possible. When we encounter an element that
# can't be deleted this way, we skip over it for the rest of the pass,
# and continue to its left. This lets us finish a pass in linear time,
# but the drawback is that we'll miss some possible deletions of
# already-skipped elements.
skipped = 0

# When every element has been deleted or skipped, the pass is complete.
while skipped < len(self.current):
# Number of remaining elements to the left of the skipped region.
# These are all candidates for attempted deletion.
candidates = len(self.current) - skipped

# Take a stable snapshot of the current sequence, so that deleting
# elements doesn't mess with our slice indices.
start = self.current

# Delete as many elements as possible (k) from the candidate
# region, from right to left. Always retain the skipped elements
# at the end. (See diagram below.)
find_integer(
lambda k: k <= i + 1 and self.consider(
start[:i + 1 - k] + start[i + 1:]
lambda k: k <= candidates and self.consider(
start[:candidates - k] + start[candidates:]
)
)
j += 1

# If we stopped because of an element we couldn't delete, enlarge
# the skipped region to include it, and continue. (If we stopped
# because we deleted everything, the loop is about to end anyway.)
skipped += 1


# This diagram shows how we use two slices to delete (k) elements from the
# candidate region, while retaining the other candidates, and retaining all of
# the skipped elements.
# <==================
# candidates skipped
# /^^^^^^^^^^^^^^^^^^^^^^^^^\ /^^^^^^^^^^^^^^^^^\
# +---+---+---+---+---+---+---+---+---+---+---+---+
# | | | | | | | | | | | | |
# +---+---+---+---+---+---+---+---+---+---+---+---+
# \_________________/ \#####/ \_________________/
# [:candidates-k] k [candidates:]
# <======
Expand Up @@ -18,6 +18,7 @@
from __future__ import division, print_function, absolute_import

from random import Random
from itertools import chain

import hypothesis.strategies as st
from hypothesis import given, example
Expand All @@ -38,3 +39,20 @@ def test_shrinks_down_to_size(m, n):

def test_will_shrink_to_zero():
assert Length.shrink([1], lambda x: True, random=Random(0)) == ()


def _concat(xs):
return tuple(chain.from_iterable(xs))


@given(st.lists(st.integers(0, 20), min_size=1))
def test_deletes_all_easily_deletable_elements(gap_sizes):
# For each "gap size" in the input, create that many 0s, followed by a 1.
# Then remove the last 1, so that there can be a gap at the end.
data = _concat([0] * g + [1] for g in gap_sizes)[:-1]
total = sum(data)

result = Length.shrink(data, lambda d: sum(d) == total)

# All 0s should have been deleted, leaving only the 1s.
assert result == (1,) * total

0 comments on commit 1652781

Please sign in to comment.