Repository: HypothesisWorks/hypothesis

Issue:
IndexError in shrinking
We (very occasionally - I've only seen it once so far in CI) see the following when running https://github.com/HypothesisWorks/hypothesis/blob/master/hypothesis-python/tests/nocover/test_boundary_exploration.py.

```python-traceback
  File "/home/david/hypothesis/hypothesis-python/src/hypothesis/internal/conjecture/engine.py", line 1325, in greedy_shrink
    self.shrink_offset_pairs()
  File "/home/david/hypothesis/hypothesis-python/src/hypothesis/internal/conjecture/engine.py", line 1581, in shrink_offset_pairs
    block_val = int_from_block(j)
  File "/home/david/hypothesis/hypothesis-python/src/hypothesis/internal/conjecture/engine.py", line 1551, in int_from_block
    return int_from_bytes(current[i])
IndexError: list index out of range
```

Currently adding `@reproduce_failure('3.57.0', 'AXicY2BgYARCECICgFQxglQDAADfAAg=')` to the test will trigger.

I believe that this is because a mismatch between the number of blocks on `shrink_target` and current, and that the following patch fixes the problem:

```diff
diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/engine.py b/hypothesis-python/src/hypothesis/internal/conjecture/engine.py
index 69aaa6f..2f37361 100644
--- a/hypothesis-python/src/hypothesis/internal/conjecture/engine.py
+++ b/hypothesis-python/src/hypothesis/internal/conjecture/engine.py
@@ -1574,21 +1574,20 @@ class Shrinker(object):
             return self.incorporate_new_buffer(buffer)

         i = 0
-        while i < len(self.blocks):
+        while i < len(current):
             if not self.is_shrinking_block(i) and int_from_block(i) > 0:
                 j = i + 1
-                while j < len(self.shrink_target.blocks):
+                while j < len(current):
                     block_val = int_from_block(j)
                     i_block_val = int_from_block(i)
                     if not self.is_shrinking_block(j) \
                        and block_val > 0 and i_block_val > 0:
                         offset = min(int_from_block(i),
                                      int_from_block(j))
-                        # Save current before shrinking
-                        current = [self.shrink_target.buffer[u:v]
-                                   for u, v in self.blocks]
                         minimize_int(
                             offset, lambda o: reoffset_pair((i, j), o))
+                        current = [self.shrink_target.buffer[u:v]
+                                   for u, v in self.blocks]
                     j += 1
             i += 1
```

The reason this is an issue rather than a PR is that I cannot for the life of me come up with a simple reproduction of this bug after an hour or two of poking at it, and I don't want to go further down this rabbit hole right now.
Flaky test: test_can_find_unique_lists_of_non_set_order
Observed in https://ci.appveyor.com/project/DRMacIver/hypothesis/build/1.0.1382/job/iv87knukf0lst2hk for #1580.

I was able to reproduce this locally after around 2000 tests.

(Turns out installing `pytest-repeat` is much more convenient than writing my own shell-script loops.)


Discussion:
I don’t know if this is significant, but I am unable to reproduce the error under Python 3.6.3 in Alpine, Python 3.6.4 in macOS – both with hypothesis 3.57.0.

Wherever I put the `@reproduce_failure` decorator, I get a DidNotReproduce error. Traceback below.

<details>

```
$ py.test example.py
======================================= test session starts ========================================
platform darwin -- Python 3.6.4, pytest-3.5.1, py-1.5.3, pluggy-0.6.0
rootdir: /private/var/folders/tq/m5sckx6j72q9xr115j1fkhz00000gn/T/tmp.JLQaeKE9, inifile:
plugins: hypothesis-3.57.0
collected 1 item

example.py F                                                                                 [100%]

============================================= FAILURES =============================================
_____________________________ test_explore_arbitrary_function[strat0] ______________________________

x = '00/0ı'

    def predicate(x):
        try:
>           return cache[x]
E           KeyError: '00/0ı'

example.py:24: KeyError

During handling of the above exception, another exception occurred:

arguments = (), kwargs = {'strat': text(min_size=5)}, __tracebackhide__ = True
settings = settings(buffer_size=8192, database=DirectoryBasedExampleDatabase('/private/var/folders/tq/m5sckx6j72q9xr115j1fkhz0000..._health_check=[HealthCheck.too_slow, HealthCheck.hung_test], timeout=60, use_coverage=True, verbosity=Verbosity.normal)
random = <random.Random object at 0x7fc446856a18>
processed_args = ((), {'strat': text(min_size=5)}, <function default_new_style_executor at 0x10b7a2ae8>, <hypothesis.core.WithRunner object at 0x10b864048>)
test_runner = <function default_new_style_executor at 0x10b7a2ae8>
search_strategy = <hypothesis.core.WithRunner object at 0x10b864048>, runner = text(min_size=5)

    @impersonate(test)
    @define_function_signature(
        test.__name__, test.__doc__, argspec
    )
    def wrapped_test(*arguments, **kwargs):
        # Tell pytest to omit the body of this function from tracebacks
        __tracebackhide__ = True

        if getattr(test, 'is_hypothesis_test', False):
            note_deprecation(
                'You have applied @given to a test more than once. In '
                'future this will be an error. Applying @given twice '
                'wraps the test twice, which can be extremely slow. A '
                'similar effect can be gained by combining the arguments '
                'of the two calls to given. For example, instead of '
                '@given(booleans()) @given(integers()), you could write '
                '@given(booleans(), integers())'
            )

        settings = wrapped_test._hypothesis_internal_use_settings

        random = get_random_for_wrapped_test(test, wrapped_test)

        if infer in generator_kwargs.values():
            hints = get_type_hints(test)
        for name in [name for name, value in generator_kwargs.items()
                     if value is infer]:
            if name not in hints:
                raise InvalidArgument(
                    'passed %s=infer for %s, but %s has no type annotation'
                    % (name, test.__name__, name))
            generator_kwargs[name] = st.from_type(hints[name])

        processed_args = process_arguments_to_given(
            wrapped_test, arguments, kwargs, generator_arguments,
            generator_kwargs, argspec, test, settings
        )
        arguments, kwargs, test_runner, search_strategy = processed_args

        runner = getattr(search_strategy, 'runner', None)
        if isinstance(runner, TestCase) and test.__name__ in dir(TestCase):
            msg = ('You have applied @given to the method %s, which is '
                   'used by the unittest runner but is not itself a test.'
                   '  This is not useful in any way.' % test.__name__)
            fail_health_check(settings, msg, HealthCheck.not_a_test_method)
        if bad_django_TestCase(runner):  # pragma: no cover
            # Covered by the Django tests, but not the pytest coverage task
            raise InvalidArgument(
                'You have applied @given to a method on %s, but this '
                'class does not inherit from the supported versions in '
                '`hypothesis.extra.django`.  Use the Hypothesis variants '
                'to ensure that each example is run in a separate '
                'database transaction.' % qualname(type(runner))
            )

        state = StateForActualGivenExecution(
            test_runner, search_strategy, test, settings, random,
            had_seed=wrapped_test._hypothesis_internal_use_seed
        )

        reproduce_failure = \
            wrapped_test._hypothesis_internal_use_reproduce_failure

        if reproduce_failure is not None:
            expected_version, failure = reproduce_failure
            if expected_version != __version__:
                raise InvalidArgument((
                    'Attempting to reproduce a failure from a different '
                    'version of Hypothesis. This failure is from %s, but '
                    'you are currently running %r. Please change your '
                    'Hypothesis version to a matching one.'
                ) % (expected_version, __version__))
            try:
                state.execute(ConjectureData.for_buffer(
                    decode_failure(failure)),
>                   print_example=True, is_final=True,
                )

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/core.py:1006:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <hypothesis.core.StateForActualGivenExecution object at 0x10b864080>
data = <hypothesis.internal.conjecture.data.ConjectureData object at 0x10b864390>
print_example = True, is_final = True, expected_failure = None, collect = False

    def execute(
        self, data,
        print_example=False,
        is_final=False,
        expected_failure=None, collect=False,
    ):
        text_repr = [None]
        if self.settings.deadline is None:
            test = self.test
        else:
            @proxies(self.test)
            def test(*args, **kwargs):
                self.__test_runtime = None
                initial_draws = len(data.draw_times)
                start = benchmark_time()
                result = self.test(*args, **kwargs)
                finish = benchmark_time()
                internal_draw_time = sum(data.draw_times[initial_draws:])
                runtime = (finish - start - internal_draw_time) * 1000
                self.__test_runtime = runtime
                if self.settings.deadline is not_set:
                    if (
                        not self.__warned_deadline and
                        runtime >= 200
                    ):
                        self.__warned_deadline = True
                        note_deprecation((
                            'Test took %.2fms to run. In future the default '
                            'deadline setting will be 200ms, which will '
                            'make this an error. You can set deadline to '
                            'an explicit value of e.g. %d to turn tests '
                            'slower than this into an error, or you can set '
                            'it to None to disable this check entirely.') % (
                                runtime, ceil(runtime / 100) * 100,
                        ))
                else:
                    current_deadline = self.settings.deadline
                    if not is_final:
                        current_deadline *= 1.25
                    if runtime >= current_deadline:
                        raise DeadlineExceeded(runtime, self.settings.deadline)
                return result

        def run(data):
            if not hasattr(data, 'can_reproduce_example_from_repr'):
                data.can_reproduce_example_from_repr = True
            with self.settings:
                with BuildContext(data, is_final=is_final):
                    rnd_module.seed(0)
                    args, kwargs = data.draw(self.search_strategy)
                    if expected_failure is not None:
                        text_repr[0] = arg_string(test, args, kwargs)

                    if print_example:
                        example = '%s(%s)' % (
                            test.__name__, arg_string(test, args, kwargs))
                        try:
                            ast.parse(example)
                        except SyntaxError:
                            data.can_reproduce_example_from_repr = False
                        report('Falsifying example: %s' % (example,))
                    elif current_verbosity() >= Verbosity.verbose:
                        report(
                            lambda: 'Trying example: %s(%s)' % (
                                test.__name__, arg_string(test, args, kwargs)))

                    if self.collector is None or not collect:
                        return test(*args, **kwargs)
                    else:  # pragma: no cover
                        try:
                            self.collector.start()
                            return test(*args, **kwargs)
                        finally:
                            self.collector.stop()

>       result = self.test_runner(data, run)

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/core.py:584:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

data = <hypothesis.internal.conjecture.data.ConjectureData object at 0x10b864390>
function = <function StateForActualGivenExecution.execute.<locals>.run at 0x10b8fb7b8>

    def default_new_style_executor(data, function):
>       return function(data)

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/executors.py:58:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

data = <hypothesis.internal.conjecture.data.ConjectureData object at 0x10b864390>

    def run(data):
        if not hasattr(data, 'can_reproduce_example_from_repr'):
            data.can_reproduce_example_from_repr = True
        with self.settings:
            with BuildContext(data, is_final=is_final):
                rnd_module.seed(0)
                args, kwargs = data.draw(self.search_strategy)
                if expected_failure is not None:
                    text_repr[0] = arg_string(test, args, kwargs)

                if print_example:
                    example = '%s(%s)' % (
                        test.__name__, arg_string(test, args, kwargs))
                    try:
                        ast.parse(example)
                    except SyntaxError:
                        data.can_reproduce_example_from_repr = False
                    report('Falsifying example: %s' % (example,))
                elif current_verbosity() >= Verbosity.verbose:
                    report(
                        lambda: 'Trying example: %s(%s)' % (
                            test.__name__, arg_string(test, args, kwargs)))

                if self.collector is None or not collect:
>                   return test(*args, **kwargs)

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/core.py:576:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

strat = text(min_size=5), data = data(...)

    @pytest.mark.parametrize('strat', [st.text(min_size=5)])
    @settings(
        max_shrinks=0, deadline=None, suppress_health_check=[
            HealthCheck.too_slow, HealthCheck.hung_test]
    )
    @reproduce_failure('3.57.0', 'AXicY2BgYARCECICgFQxglQDAADfAAg=')
    @given(st.data())
    def test_explore_arbitrary_function(strat, data):
        cache = {}

        def predicate(x):
            try:
                return cache[x]
            except KeyError:
                return cache.setdefault(x, data.draw(st.booleans(), label=repr(x)))

        try:
            find(
                strat, predicate,
                settings=settings(
>                   database=None, verbosity=Verbosity.quiet, max_shrinks=1000)
            )

example.py:32:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

specifier = text(min_size=5)
condition = <function test_explore_arbitrary_function.<locals>.predicate at 0x10b8fb950>
settings = settings(buffer_size=8192, database=None, deadline=None, derandomize=False, max_examples=100, max_shrinks=1000, phases...althCheck.large_base_example, HealthCheck.not_a_test_method], timeout=60, use_coverage=True, verbosity=Verbosity.quiet)
random = <random.Random object at 0x7fc4441a4818>, database_key = None

    def find(
        specifier,  # type: SearchStrategy
        condition,  # type: Callable[[Any], bool]
        settings=None,  # type: Settings
        random=None,   # type: Any
        database_key=None,  # type: bytes
    ):
        # type: (...) -> Any
        """Returns the minimal example from the given strategy ``specifier`` that
        matches the predicate function ``condition``."""
        settings = settings or Settings(
            max_examples=2000,
            max_shrinks=2000,
        )
        settings = Settings(settings, suppress_health_check=HealthCheck.all())

        if database_key is None and settings.database is not None:
            database_key = function_digest(condition)

        if not isinstance(specifier, SearchStrategy):
            raise InvalidArgument(
                'Expected SearchStrategy but got %r of type %s' % (
                    specifier, type(specifier).__name__
                ))
        specifier.validate()

        search = specifier

        random = random or new_random()
        successful_examples = [0]
        last_data = [None]
        last_repr = [None]

        def template_condition(data):
            with BuildContext(data):
                try:
                    data.is_find = True
                    result = data.draw(search)
                    data.note(result)
                    success = condition(result)
                except UnsatisfiedAssumption:
                    data.mark_invalid()

            if success:
                successful_examples[0] += 1

            if settings.verbosity >= Verbosity.verbose:
                if not successful_examples[0]:
                    report(
                        u'Tried non-satisfying example %s' % (nicerepr(result),))
                elif success:
                    if successful_examples[0] == 1:
                        last_repr[0] = nicerepr(result)
                        report(u'Found satisfying example %s' % (last_repr[0],))
                        last_data[0] = data
                    elif (
                        sort_key(hbytes(data.buffer)) <
                        sort_key(last_data[0].buffer)
                    ) and nicerepr(result) != last_repr[0]:
                        last_repr[0] = nicerepr(result)
                        report(u'Shrunk example to %s' % (last_repr[0],))
                        last_data[0] = data
            if success and not data.frozen:
                data.mark_interesting()
        start = time.time()
        runner = ConjectureRunner(
            template_condition, settings=settings, random=random,
            database_key=database_key,
        )
>       runner.run()

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/core.py:1153:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <hypothesis.internal.conjecture.engine.ConjectureRunner object at 0x10b864fd0>

    def run(self):
        with self.settings:
            try:
>               self._run()

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/internal/conjecture/engine.py:438:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <hypothesis.internal.conjecture.engine.ConjectureRunner object at 0x10b864fd0>

    def _run(self):
        self.start_time = benchmark_time()

        self.reuse_existing_examples()
        self.generate_new_examples()
>       self.shrink_interesting_examples()

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/internal/conjecture/engine.py:773:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <hypothesis.internal.conjecture.engine.ConjectureRunner object at 0x10b864fd0>

    def shrink_interesting_examples(self):
        """If we've found interesting examples, try to replace each of them
            with a minimal interesting example with the same interesting_origin.

            We may find one or more examples with a new interesting_origin
            during the shrink process. If so we shrink these too.
            """
        if (
            Phase.shrink not in self.settings.phases or
            not self.interesting_examples
        ):
            return

        for prev_data in sorted(
            self.interesting_examples.values(),
            key=lambda d: sort_key(d.buffer)
        ):
            assert prev_data.status == Status.INTERESTING
            data = ConjectureData.for_buffer(prev_data.buffer)
            self.test_function(data)
            if data.status != Status.INTERESTING:
                self.exit_with(ExitReason.flaky)

        self.clear_secondary_key()

        while len(self.shrunk_examples) < len(self.interesting_examples):
            target, example = min([
                (k, v) for k, v in self.interesting_examples.items()
                if k not in self.shrunk_examples],
                key=lambda kv: (sort_key(kv[1].buffer), sort_key(repr(kv[0]))),
            )
            self.debug('Shrinking %r' % (target,))

            def predicate(d):
                if d.status < Status.INTERESTING:
                    return False
                return d.interesting_origin == target

>           self.shrink(example, predicate)

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/internal/conjecture/engine.py:815:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <hypothesis.internal.conjecture.engine.ConjectureRunner object at 0x10b864fd0>
example = <hypothesis.internal.conjecture.data.ConjectureData object at 0x10b7e2c50>
predicate = <function ConjectureRunner.shrink_interesting_examples.<locals>.predicate at 0x10b3258c8>

    def shrink(self, example, predicate):
        s = self.new_shrinker(example, predicate)
>       s.shrink()

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/internal/conjecture/engine.py:851:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <hypothesis.internal.conjecture.engine.Shrinker object at 0x10b39ec88>

    def shrink(self):
        """Run the full set of shrinks and update shrink_target.

            This method is "mostly idempotent" - calling it twice is unlikely to
            have any effect, though it has a non-zero probability of doing so.
            """
        # We assume that if an all-zero block of bytes is an interesting
        # example then we're not going to do better than that.
        # This might not technically be true: e.g. for integers() | booleans()
        # the simplest example is actually [1, 0]. Missing this case is fairly
        # harmless and this allows us to make various simplifying assumptions
        # about the structure of the data (principally that we're never
        # operating on a block of all zero bytes so can use non-zeroness as a
        # signpost of complexity).
        if (
            not any(self.shrink_target.buffer) or
            self.incorporate_new_buffer(hbytes(len(self.shrink_target.buffer)))
        ):
            return

>       self.greedy_shrink()

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/internal/conjecture/engine.py:1276:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <hypothesis.internal.conjecture.engine.Shrinker object at 0x10b39ec88>

    def greedy_shrink(self):
        """Run a full set of greedy shrinks (that is, ones that will only ever
            move to a better target) and update shrink_target appropriately.

            This method iterates to a fixed point and so is idempontent - calling
            it twice will have exactly the same effect as calling it once.
            """
        run_expensive_shrinks = False

        prev = None
        while prev is not self.shrink_target:
            prev = self.shrink_target

            # We reset our tracking of what changed at the beginning of the
            # loop so that we don't get distracted by things that change once
            # and then are stable thereafter.
            self.clear_change_tracking()

            self.remove_discarded()
            self.adaptive_example_deletion()
            self.zero_draws()
            self.minimize_duplicated_blocks()
            self.minimize_individual_blocks()
            self.reorder_blocks()
>           self.lower_dependent_block_pairs()

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/internal/conjecture/engine.py:1303:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <hypothesis.internal.conjecture.engine.Shrinker object at 0x10b39ec88>

    def lower_dependent_block_pairs(self):
        """This is a fairly specific shrink pass that is mostly specialised for
            our integers strategy, though is probably useful in other places.

            It looks for adjacent pairs of blocks where lowering the value of the
            first changes the size of the second. Where this happens, we may lose
            interestingness because this takes the prefix rather than the suffix
            of the next block, so if lowering the block produces an uninteresting
            value and this change happens, we try replacing the second block with
            its suffix and shrink again.

            For example suppose we had:

                m = data.draw_bits(8)
                n = data.draw_bits(m)

            And initially we draw m = 9, n = 1. This gives us the bytes [9, 0, 1].
            If we lower 9 to 8 then we now read [9, 0], because the block size of
            the n has changed. This pass allows us to also try [9, 1], which
            corresponds to m=8, n=1 as desired.

            This should *mostly* be handled by the minimize_individual_blocks pass,
            but that won't always work because its length heuristic can be wrong if
            the changes to the next block have knock on size changes, while this
            one triggers more reliably.
            """
        self.debug('Lowering adjacent pairs of dependent blocks')
        i = 0
        while i + 1 < len(self.blocks):
            u, v = self.blocks[i]
            i += 1

            b = int_from_bytes(self.shrink_target.buffer[u:v])
            if b > 0:
                attempt = bytearray(self.shrink_target.buffer)
                attempt[u:v] = int_to_bytes(b - 1, v - u)
                attempt = hbytes(attempt)
>               shrunk = self.cached_test_function(attempt)

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/internal/conjecture/engine.py:2127:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <hypothesis.internal.conjecture.engine.Shrinker object at 0x10b39ec88>
buffer = b'\x01\x00\x00\x01\x00\x00\x01\x00\x01\x01\x00\x00\x01\x01\x00\x01\x01\x00'

    def cached_test_function(self, buffer):
>       result = self.__engine.cached_test_function(buffer)

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/internal/conjecture/engine.py:1249:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <hypothesis.internal.conjecture.engine.ConjectureRunner object at 0x10b864fd0>
buffer = b'\x01\x00\x00\x01\x00\x00\x01\x00\x01\x01\x00\x00\x01\x01\x00\x01\x01\x00'

    def cached_test_function(self, buffer):
        node_index = 0
        for i in hrange(self.settings.buffer_size):
            try:
                c = self.forced[node_index]
            except KeyError:
                if i < len(buffer):
                    c = buffer[i]
                else:
                    c = 0
            try:
                node_index = self.tree[node_index][c]
            except KeyError:
                break
            node = self.tree[node_index]
            if isinstance(node, ConjectureData):
                return node
        result = ConjectureData.for_buffer(buffer)
>       self.test_function(result)

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/internal/conjecture/engine.py:914:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <hypothesis.internal.conjecture.engine.ConjectureRunner object at 0x10b864fd0>
data = <hypothesis.internal.conjecture.data.ConjectureData object at 0x10ba5fd30>

    def test_function(self, data):
        if benchmark_time() - self.start_time >= HUNG_TEST_TIME_LIMIT:
            fail_health_check(self.settings, (
                'Your test has been running for at least five minutes. This '
                'is probably not what you intended, so by default Hypothesis '
                'turns it into an error.'
            ), HealthCheck.hung_test)

        self.call_count += 1
        try:
            self._test_function(data)
            data.freeze()
        except StopTest as e:
            if e.testcounter != data.testcounter:
                self.save_buffer(data.buffer)
>               raise e

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/internal/conjecture/engine.py:158:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <hypothesis.internal.conjecture.engine.ConjectureRunner object at 0x10b864fd0>
data = <hypothesis.internal.conjecture.data.ConjectureData object at 0x10ba5fd30>

    def test_function(self, data):
        if benchmark_time() - self.start_time >= HUNG_TEST_TIME_LIMIT:
            fail_health_check(self.settings, (
                'Your test has been running for at least five minutes. This '
                'is probably not what you intended, so by default Hypothesis '
                'turns it into an error.'
            ), HealthCheck.hung_test)

        self.call_count += 1
        try:
>           self._test_function(data)

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/internal/conjecture/engine.py:153:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

data = <hypothesis.internal.conjecture.data.ConjectureData object at 0x10ba5fd30>

    def template_condition(data):
        with BuildContext(data):
            try:
                data.is_find = True
                result = data.draw(search)
                data.note(result)
>               success = condition(result)

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/core.py:1123:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

x = '00/0ı'

    def predicate(x):
        try:
            return cache[x]
        except KeyError:
>           return cache.setdefault(x, data.draw(st.booleans(), label=repr(x)))

example.py:26:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = data(...), strategy = booleans(), label = "'00/0ı'"

    def draw(self, strategy, label=None):
>       result = self.data.draw(strategy)

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/strategies.py:1935:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <hypothesis.internal.conjecture.data.ConjectureData object at 0x10b864390>
strategy = booleans(), label = None

    def draw(self, strategy, label=None):
        if self.is_find and not strategy.supports_find:
            raise InvalidArgument((
                'Cannot use strategy %r within a call to find (presumably '
                'because it would be invalid after the call had ended).'
            ) % (strategy,))

        if strategy.is_empty:
            self.mark_invalid()

        if self.depth >= MAX_DEPTH:
            self.mark_invalid()

        if self.depth == 0 and not IN_COVERAGE_TESTS:  # pragma: no cover
            original_tracer = sys.gettrace()
            try:
                sys.settrace(None)
>               return self.__draw(strategy, label=label)

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/internal/conjecture/data.py:162:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <hypothesis.internal.conjecture.data.ConjectureData object at 0x10b864390>
strategy = booleans(), label = 15500253515903991915

    def __draw(self, strategy, label):
        at_top_level = self.depth == 0
        if label is None:
            label = strategy.label
        self.start_example(label=label)
        try:
            if not at_top_level:
                return strategy.do_draw(self)
            else:
                start_time = benchmark_time()
                try:
>                   return strategy.do_draw(self)

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/internal/conjecture/data.py:179:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = booleans(), data = <hypothesis.internal.conjecture.data.ConjectureData object at 0x10b864390>

    def do_draw(self, data):
>       return data.draw(self.wrapped_strategy)

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/searchstrategy/lazy.py:160:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <hypothesis.internal.conjecture.data.ConjectureData object at 0x10b864390>
strategy = BoolStrategy(), label = None

    def draw(self, strategy, label=None):
        if self.is_find and not strategy.supports_find:
            raise InvalidArgument((
                'Cannot use strategy %r within a call to find (presumably '
                'because it would be invalid after the call had ended).'
            ) % (strategy,))

        if strategy.is_empty:
            self.mark_invalid()

        if self.depth >= MAX_DEPTH:
            self.mark_invalid()

        if self.depth == 0 and not IN_COVERAGE_TESTS:  # pragma: no cover
            original_tracer = sys.gettrace()
            try:
                sys.settrace(None)
                return self.__draw(strategy, label=label)
            finally:
                sys.settrace(original_tracer)
        else:
>           return self.__draw(strategy, label=label)

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/internal/conjecture/data.py:166:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <hypothesis.internal.conjecture.data.ConjectureData object at 0x10b864390>
strategy = BoolStrategy(), label = 15500253515903991915

    def __draw(self, strategy, label):
        at_top_level = self.depth == 0
        if label is None:
            label = strategy.label
        self.start_example(label=label)
        try:
            if not at_top_level:
>               return strategy.do_draw(self)

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/internal/conjecture/data.py:175:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = BoolStrategy()
data = <hypothesis.internal.conjecture.data.ConjectureData object at 0x10b864390>

    def do_draw(self, data):
>       return d.boolean(data)

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/searchstrategy/misc.py:37:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

data = <hypothesis.internal.conjecture.data.ConjectureData object at 0x10b864390>

    def boolean(data):
>       return bool(data.draw_bits(1))

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/internal/conjecture/utils.py:178:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <hypothesis.internal.conjecture.data.ConjectureData object at 0x10b864390>, n = 1

    def draw_bits(self, n):
        self.__assert_not_frozen('draw_bits')
        if n == 0:
            result = 0
        elif n % 8 == 0:
            return int_from_bytes(self.draw_bytes(n // 8))
        else:
            n_bytes = (n // 8) + 1
>           self.__check_capacity(n_bytes)

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/internal/conjecture/data.py:258:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <hypothesis.internal.conjecture.data.ConjectureData object at 0x10b864390>, n = 1

    def __check_capacity(self, n):
        if self.index + n > self.max_length:
            self.overdraw = self.index + n - self.max_length
            self.status = Status.OVERRUN
            self.freeze()
>           raise StopTest(self.testcounter)
E           hypothesis.errors.StopTest: 0

/Users/alexwlchan/.virtualenvs/tempenv-2283124383c54/lib/python3.6/site-packages/hypothesis/internal/conjecture/data.py:284: StopTest

During handling of the above exception, another exception occurred:

strat = text(min_size=5)

    @pytest.mark.parametrize('strat', [st.text(min_size=5)])
>   @settings(
        max_shrinks=0, deadline=None, suppress_health_check=[
            HealthCheck.too_slow, HealthCheck.hung_test]
    )
    @reproduce_failure('3.57.0', 'AXicY2BgYARCECICgFQxglQDAADfAAg=')
    @given(st.data())
    def test_explore_arbitrary_function(strat, data):
E   hypothesis.errors.DidNotReproduce: The shape of the test data has changed in some way from where this blob was defined. Are you sure you're running the same test?

example.py:13: DidNotReproduce
-------------------------------------------- Hypothesis --------------------------------------------
Falsifying example: test_explore_arbitrary_function(strat=text(min_size=5), data=data(...))
===================================== 1 failed in 1.79 seconds =====================================
```

</details>
Interesting. I think I saw this under pypy, but I'm surprised it's python version specific. Clearly we've not ironed out the full range of compatibility behaviour.
PyPy 5.10.0, Python 2.7.13 on Alpine Linux:

```
/repo # py.test example.py
======================================= test session starts ========================================
platform linux2 -- Python 2.7.13[pypy-5.10.0-final], pytest-3.5.1, py-1.5.3, pluggy-0.6.0
rootdir: /repo, inifile:
plugins: hypothesis-3.57.0
collected 1 item

example.py F                                                                                 [100%]

============================================= FAILURES =============================================
_____________________________ test_explore_arbitrary_function[strat0] ______________________________

strat = text(min_size=5)

    @reproduce_failure('3.57.0', 'AXicY2BgYARCECICgFQxglQDAADfAAg=')
>   @pytest.mark.parametrize('strat', [st.text(min_size=5)])
    @settings(
        max_shrinks=0, deadline=None, suppress_health_check=[
            HealthCheck.too_slow, HealthCheck.hung_test]
    )
    @given(st.data())
    def test_explore_arbitrary_function(strat, data):
E   DidNotReproduce: Expected the test to raise an error, but it completed successfully.

example.py:13: DidNotReproduce
-------------------------------------------- Hypothesis --------------------------------------------
Falsifying example: test_explore_arbitrary_function(strat=text(min_size=5), data=data(...))
===================================== 1 failed in 0.49 seconds =====================================
```
This is a specific manifestation of the general bug #1299, I think.
Debug output from the failing pass:
```
Shrink Pass shrink_offset_pairs
15 bytes [[[1, 0], [1, 0], [1, [[1, [1, 257]], [1, [0, 32]], 0]], 0]] -> Status.VALID, ['', 'ı\x10']
15 bytes [[[1, 0], [1, 0], [1, [[1, [1, 65537]], [1, [0, 32]], 0]], 0]] -> Status.VALID, ['', '𐠱\x10']
15 bytes [[[1, 0], [1, 0], [1, [[1, [1, 65792]], [1, [0, 32]], 0]], 0]] -> Status.VALID, ['', '𐤰\x10']
15 bytes [[[1, 0], [1, 0], [1, [[1, [1, 65793]], [1, [0, 31]], 0]], 0]] -> Status.VALID, ['', '𐤱\x11']
15 bytes [[[1, 0], [1, [[1, [0, 1]], [1, [0, 1]], [1, [1, 8192]], 0]]]] -> Status.OVERRUN, 
15 bytes [[[1, 0], [1, [[1, [0, 1]], [1, [1, 257]], [160, [0, 0]], 0]]]] -> Status.OVERRUN, 
15 bytes [[[1, 0], [1, [[1, [0, 1]], [1, [1, 65537]], [160, [0, 0]], 0]]]] -> Status.OVERRUN, 
13 bytes [[[1, 0], [1, [[1, [0, 1]], [1, [1, 65793]], [160, 1]]]]] -> Status.OVERRUN, 
15 bytes [[[1, 0], [1, [[1, [1, 65792]], 0]], [1, [[1, [0, 32]], 0]], 0]] -> Status.VALID, ['', '𐤰', '\x10']
15 bytes [[[1, 0], [1, [[1, [1, 65792]], [1, [0, 1]], [160, [0, 0]], 0]]]] -> Status.OVERRUN, 
15 bytes [[[1, 0], [1, [[1, [1, 0]], [1, [1, 40735]], 0]], 0]] -> Status.VALID, ['', '0齏']
15 bytes [[[1, 0], [1, [[1, [1, 1]], [1, [1, 40736]], 0]], 0]] -> Status.VALID, ['', '1齐']
15 bytes [[[1, 0], [1, [[1, [1, 257]], [1, [1, 40992]], 0]], 0]] -> Status.VALID, ['', 'ıꁐ']
15 bytes [[[1, 0], [1, [[1, [1, 128]], [1, [1, 40863]], 0]], 0]] -> Status.VALID, ['', '°鿏']
15 bytes [[[1, 0], [1, [[1, [1, 32896]], [1, [1, 73631]], 0]], 0]] -> Status.VALID, ['', '肰\U000127cf']
15 bytes [[[1, 0], [1, [[1, [1, 65791]], [1, [1, 106526]], 0]], 0]] -> Status.VALID, ['', '𐤯\U0001a84e']
15 bytes [[[1, 0], [1, [[1, [1, 65792]], [1, [1, 106527]], 0]], 0]] -> Status.VALID, ['', '𐤰\U0001a84f']
Shrunk example to ['', '𐤱', '\x11']
15 bytes [[[1, 0], [1, [[1, [1, 65793]], 0]], [1, [[1, [0, 31]], 0]], 0]] -> Status.INTERESTING (None), ['', '𐤱', '\x11']
Shrink Pass shrink_offset_pairs completed.
```
The test seems to be sensitive to the iteration order of sets, which is scary.

On paper this shouldn't translate to the test itself being flaky, but the connection is too suspicious to ignore.
I've found a buffer that consistently produces the internal assertion failure, when run with `PYTHONHASHSEED=0`:

```python
def test_shrinker_assertion(monkeypatch):
    buffer = b'\x01\x00\x01\x01\x01\x00\x01\x01\x01\x00\x01\x01\x01\x00\x01\x01\x00\x00'

    monkeypatch.setattr(
        ConjectureRunner, 'generate_new_examples',
        lambda runner: runner.test_function(
            ConjectureData.for_buffer(buffer)
        )
    )

    @run_to_buffer
    def x(data):
        y = data.draw(st.lists(st.text(), unique=True))
        if list(set(reversed(y))) != y:
            data.mark_interesting()

[_seeded.txt](https://github.com/HypothesisWorks/hypothesis/files/2386552/_seeded.txt)

This is output from my run that found this buffer. (I have some local changes applied for extra debugging output.)
[_blocks.txt](https://github.com/HypothesisWorks/hypothesis/files/2386633/_blocks.txt)

It looks like a successful shrink is changing the example's block structure, splitting it into more, shorter blocks.

This causes `j` to run past the length of the original block list, which then causes problems when we immediately use it as an index into that list.

It should be possible to contrive an artificial test case that exhibits this behaviour, without having to rely on set iteration order.
Hmm, it's more subtle than that, unfortunately. My first attempt at an artificial test case didn't fail.

```python
def test_shrink_offset_pairs_failure(monkeypatch):
    buffer = hbytes([0, 235, 0, 0, 255, 0, 0, 0, 1])

    monkeypatch.setattr(
        ConjectureRunner, 'generate_new_examples',
        lambda runner: runner.test_function(
            ConjectureData.for_buffer(buffer)
        )
    )

    @run_to_buffer
    def x(data):
        data.draw_bytes(1)
        x = data.draw_bytes(1)[0]
        data.draw_bytes(1)
        data.draw_bytes(1)
        y = data.draw_bytes(1)[0]
        if y < 200:
            data.draw_bytes(1)
            data.draw_bytes(1)
            data.draw_bytes(1)
        else:
            data.draw_bytes(3)
        z = data.draw_bytes(1)[0]

        if x >= 10 and y - x == 20 and z > 0:
            data.mark_interesting()

```

This one seems to do the trick:

```python
def test_shrink_offset_pairs_failure(monkeypatch):
    buffer = hbytes([235, 0, 0, 255])

    monkeypatch.setattr(
        ConjectureRunner, 'generate_new_examples',
        lambda runner: runner.test_function(
            ConjectureData.for_buffer(buffer)
        )
    )

    @run_to_buffer
    def x(data):
        x = data.draw_bytes(1)[0]
        if x == 10:
            data.draw_bytes(1)
            data.draw_bytes(1)
        else:
            data.draw_bytes(2)
        y = data.draw_bytes(1)[0]

        if x >= 10 and y - x == 20:
            data.mark_interesting()
```

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/engine.py b/hypothesis-python/src/hypothesis/internal/conjecture/engine.py
--- a/hypothesis-python/src/hypothesis/internal/conjecture/engine.py
+++ b/hypothesis-python/src/hypothesis/internal/conjecture/engine.py
@@ -1904,7 +1904,8 @@ def reoffset(o):
             self.clear_change_tracking()
 
     def shrink_offset_pairs(self):
-        """Lower any two blocks offset from each other the same ammount.
+        """Lowers pairs of blocks that need to maintain a constant difference
+        between their respective values.
 
         Before this shrink pass, two blocks explicitly offset from each
         other would not get minimized properly:
@@ -1914,12 +1915,13 @@ def shrink_offset_pairs(self):
 
         This expensive (O(n^2)) pass goes through every pair of non-zero
         blocks in the current shrink target and sees if the shrink
-        target can be improved by applying an offset to both of them.
+        target can be improved by applying a negative offset to both of them.
         """
-        current = [self.shrink_target.buffer[u:v] for u, v in self.blocks]
 
         def int_from_block(i):
-            return int_from_bytes(current[i])
+            u, v = self.blocks[i]
+            block_bytes = self.shrink_target.buffer[u:v]
+            return int_from_bytes(block_bytes)
 
         def block_len(i):
             u, v = self.blocks[i]
@@ -1951,16 +1953,13 @@ def reoffset_pair(pair, o):
         while i < len(self.blocks):
             if self.is_payload_block(i) and int_from_block(i) > 0:
                 j = i + 1
-                while j < len(self.shrink_target.blocks):
+                while j < len(self.blocks):
                     block_val = int_from_block(j)
                     i_block_val = int_from_block(i)
                     if self.is_payload_block(j) \
                        and block_val > 0 and i_block_val > 0:
                         offset = min(int_from_block(i),
                                      int_from_block(j))
-                        # Save current before shrinking
-                        current = [self.shrink_target.buffer[u:v]
-                                   for u, v in self.blocks]
                         Integer.shrink(
                             offset, lambda o: reoffset_pair((i, j), o),
                             random=self.random


Test patch:
diff --git a/hypothesis-python/tests/cover/test_conjecture_engine.py b/hypothesis-python/tests/cover/test_conjecture_engine.py
--- a/hypothesis-python/tests/cover/test_conjecture_engine.py
+++ b/hypothesis-python/tests/cover/test_conjecture_engine.py
@@ -57,6 +57,34 @@ def run_to_buffer(f):
         return hbytes(last_data.buffer)
 
 
+def shrink_to_data(buffer):
+    """Decorator that takes a test function and an interesting input buffer,
+    and runs the shrinker on that buffer."""
+
+    def decorator(f):
+        runner = ConjectureRunner(
+            f,
+            random=Random(0),
+            settings=settings(
+                phases=[Phase.shrink],
+                database=None,
+            ),
+        )
+
+        # Explicitly test the given buffer, so that the shrinker has
+        # a starting point.
+        data = ConjectureData.for_buffer(buffer)
+        runner.test_function(data)
+        assert data.status == Status.INTERESTING, \
+            'Initial buffer was not interesting'
+
+        runner.run()
+
+        return runner.interesting_examples[data.interesting_origin]
+
+    return decorator
+
+
 def test_can_index_results():
     @run_to_buffer
     def f(data):
diff --git a/hypothesis-python/tests/cover/test_simple_collections.py b/hypothesis-python/tests/cover/test_simple_collections.py
--- a/hypothesis-python/tests/cover/test_simple_collections.py
+++ b/hypothesis-python/tests/cover/test_simple_collections.py
@@ -124,6 +124,17 @@ def test_lists_of_lower_bounded_length(n):
 
 
 def test_can_find_unique_lists_of_non_set_order():
+    # This test checks that our strategy for unique lists doesn't accidentally
+    # depend on the iteration order of sets.
+    #
+    # Unfortunately, that means that *this* test has to rely on set iteration
+    # order. That makes it tricky to debug on CPython, because set iteration
+    # order changes every time the process is launched.
+    #
+    # To get around this, define the PYTHONHASHSEED environment variable to
+    # a consistent value. This could be 0, or it could be the PYTHONHASHSEED
+    # value listed in a failure log from CI.
+
     ls = minimal(
         lists(text(), unique=True),
         lambda x: list(set(reversed(x))) != x
diff --git a/hypothesis-python/tests/nocover/test_conjecture_engine.py b/hypothesis-python/tests/nocover/test_conjecture_engine.py
--- a/hypothesis-python/tests/nocover/test_conjecture_engine.py
+++ b/hypothesis-python/tests/nocover/test_conjecture_engine.py
@@ -26,7 +26,8 @@
 from tests.common.utils import no_shrink, non_covering_examples
 from hypothesis.database import InMemoryExampleDatabase
 from hypothesis.internal.compat import hbytes, hrange, int_from_bytes
-from tests.cover.test_conjecture_engine import run_to_buffer
+from tests.cover.test_conjecture_engine import run_to_buffer, \
+    shrink_to_data
 from hypothesis.internal.conjecture.data import Status, ConjectureData
 from hypothesis.internal.conjecture.engine import RunIsComplete, \
     ConjectureRunner
@@ -153,6 +154,35 @@ def x(data):
     assert int_from_bytes(x[-2:]) in (254, 512)
 
 
+def test_shrink_offset_pairs_handles_block_structure_change():
+    """Regression test for a rare error in ``shrink_offset_pairs``.
+
+    This test should run without raising an ``IndexError`` in the
+    shrinker.
+    """
+
+    @shrink_to_data(buffer=[235, 0, 0, 255])
+    def f(data):
+        x = data.draw_bytes(1)[0]
+
+        # Change the block structure in response to a shrink improvement,
+        # to trigger the bug.
+        if x == 10:
+            data.draw_bytes(1)
+            data.draw_bytes(1)
+        else:
+            data.draw_bytes(2)
+
+        y = data.draw_bytes(1)[0]
+
+        # Require the target blocks to be non-trivial and have a fixed
+        # difference, so that the intended shrinker pass is used.
+        if x >= 10 and y - x == 20:
+            data.mark_interesting()
+
+    assert f.buffer == hbytes([10, 0, 0, 30])
+
+
 @given(st.integers(0, 255), st.integers(0, 255))
 def test_prescreen_with_masked_byte_agrees_with_results(byte_a, byte_b):
     def f(data):