From ea63609f10909037d8a93dc49335a3f75ece749c Mon Sep 17 00:00:00 2001 From: Stuart Cook Date: Fri, 28 Sep 2018 23:52:10 +1000 Subject: [PATCH 1/4] Test that the Length shrinker deletes all easily-deletable elements --- .../test_conjecture_length_shrinking.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/hypothesis-python/tests/nocover/test_conjecture_length_shrinking.py b/hypothesis-python/tests/nocover/test_conjecture_length_shrinking.py index 83623d5c1d..cf5981d683 100644 --- a/hypothesis-python/tests/nocover/test_conjecture_length_shrinking.py +++ b/hypothesis-python/tests/nocover/test_conjecture_length_shrinking.py @@ -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 @@ -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 From ca1de28e609dc132ae5ca3a6c8f410f79994e0ee Mon Sep 17 00:00:00 2001 From: Stuart Cook Date: Sat, 29 Sep 2018 20:50:39 +1000 Subject: [PATCH 2/4] Use more meaningful variable names in Length.run_step --- .../internal/conjecture/shrinking/length.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/length.py b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/length.py index a77ef2d707..f46fc89d59 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/length.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/length.py @@ -53,13 +53,13 @@ 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 + skipped = 0 + while skipped < len(self.current): + candidates = len(self.current) - skipped start = self.current 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 + skipped += 1 From 7aa3881e67b68bc54fb58d4b9ee070580beb4272 Mon Sep 17 00:00:00 2001 From: Stuart Cook Date: Tue, 9 Oct 2018 20:17:10 +1100 Subject: [PATCH 3/4] Explain how Length.run_step works --- .../internal/conjecture/shrinking/length.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/length.py b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/length.py index f46fc89d59..b5cc6967e9 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/length.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/length.py @@ -53,13 +53,51 @@ def left_is_better(self, left, right): return len(left) < len(right) def run_step(self): + # 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 <= candidates and self.consider( start[:candidates - k] + start[candidates:] ) ) + + # 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:] +# <====== From acad9cae56070b95474056e07a92036eb39db3b8 Mon Sep 17 00:00:00 2001 From: Stuart Cook Date: Sat, 29 Sep 2018 22:07:23 +1000 Subject: [PATCH 4/4] Add RELEASE.rst --- hypothesis-python/RELEASE.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 hypothesis-python/RELEASE.rst diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..9df56908b5 --- /dev/null +++ b/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.