Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explain how Length.run_step works #1608

Merged
merged 4 commits into from Oct 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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