From 9050a0cc7acfec49105781ac498780435ac1e36b Mon Sep 17 00:00:00 2001 From: "David R. MacIver" Date: Wed, 18 Jul 2018 14:49:43 +0100 Subject: [PATCH 1/3] Add a pass that minmizes a block while retaining a shared sum --- .../hypothesis/internal/conjecture/engine.py | 56 +++++++++++++++++++ .../tests/cover/test_conjecture_engine.py | 42 ++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/engine.py b/hypothesis-python/src/hypothesis/internal/conjecture/engine.py index 1af09df342..527b801b7e 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/engine.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/engine.py @@ -1339,6 +1339,7 @@ def greedy_shrink(self): self.interval_deletion_with_block_lowering() self.pass_to_interval() self.reorder_bytes() + self.minimize_block_pairs_retaining_sum() @property def blocks(self): @@ -2279,3 +2280,58 @@ def attempt(new_ordering): break else: i += 1 + + def minimize_block_pairs_retaining_sum(self): + """This pass minimizes pairs of blocks subject to the constraint that + their sum when interpreted as integers remains the same. This allow us + to normalize a number of examples that we would otherwise struggle on. + e.g. consider the following: + + m = data.draw_bits(8) + n = data.draw_bits(8) + if m + n >= 256: + data.mark_interesting() + + The ideal example for this is m=1, n=255, but we will almost never + find that without a pass like this - we would only do so if we + happened to draw n=255 by chance. + + This kind of scenario comes up reasonably often in the context of e.g. + triggering overflow behaviour. + """ + i = 0 + while i < len(self.shrink_target.blocks): + if self.is_payload_block(i): + j = i + 1 + while j < len(self.shrink_target.blocks): + u, v = self.shrink_target.blocks[i] + m = int_from_bytes(self.shrink_target.buffer[u:v]) + if m == 0: + break + r, s = self.shrink_target.blocks[j] + n = int_from_bytes(self.shrink_target.buffer[r:s]) + + if ( + s - r == v - u and + self.is_payload_block(j) + ): + def trial(x, y): + if s > len(self.shrink_target.buffer): + return False + attempt = bytearray(self.shrink_target.buffer) + try: + attempt[u:v] = int_to_bytes(x, v - u) + attempt[r:s] = int_to_bytes(y, s - r) + except OverflowError: + return False + return self.incorporate_new_buffer(attempt) + if trial(m - 1, n + 1): + m = int_from_bytes(self.shrink_target.buffer[u:v]) + n = int_from_bytes(self.shrink_target.buffer[r:s]) + + tot = m + n + minimize_int( + m, lambda x: trial(x, tot - x) + ) + j += 1 + i += 1 diff --git a/hypothesis-python/tests/cover/test_conjecture_engine.py b/hypothesis-python/tests/cover/test_conjecture_engine.py index e9d6f1d9bd..edea73e206 100644 --- a/hypothesis-python/tests/cover/test_conjecture_engine.py +++ b/hypothesis-python/tests/cover/test_conjecture_engine.py @@ -1330,3 +1330,45 @@ def f(data): runner.run() assert runner.exit_reason == ExitReason.finished + + +@pytest.mark.parametrize('lo', [0, 1, 50]) +def test_can_shrink_additively(monkeypatch, lo): + monkeypatch.setattr( + ConjectureRunner, 'generate_new_examples', + lambda self: self.test_function( + ConjectureData.for_buffer(hbytes([100, 100])))) + + @run_to_buffer + def x(data): + m = data.draw_bits(8) + n = data.draw_bits(8) + if m >= lo and m + n == 200: + data.mark_interesting() + + assert list(x) == [lo, 200 - lo] + + +def test_can_shrink_additively_losing_size(monkeypatch): + monkeypatch.setattr( + ConjectureRunner, 'generate_new_examples', + lambda self: self.test_function( + ConjectureData.for_buffer(hbytes([100, 100])))) + + monkeypatch.setattr( + Shrinker, 'shrink', lambda self: ( + self.minimize_block_pairs_retaining_sum(), + ) + ) + + @run_to_buffer + def x(data): + m = data.draw_bits(8) + if m >= 10: + if m <= 50: + data.mark_interesting() + else: + n = data.draw_bits(8) + if m + n == 200: + data.mark_interesting() + assert len(x) == 1 From 57a0babc1741bf0ccba0ac36868ef79ac202e051 Mon Sep 17 00:00:00 2001 From: "David R. MacIver" Date: Wed, 18 Jul 2018 15:46:30 +0100 Subject: [PATCH 2/3] Add release notes --- hypothesis-python/RELEASE.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 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..702b99bc66 --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,23 @@ +RELEASE_TYPE: patch + +This release improves the shrinker's ability to handle situations where there +is an additive constraint between two values. + +For example, consider the following test: + + +.. code-block:: python + + import hypothesis.strategies as st + from hypothesis import given + + @given(st.integers(), st.integers()) + def test_does_not_exceed_100(m, n): + assert m + n <= 100 + +Previously this could have failed with almost any pair ``(m, n)`` with +``0 <= m <= n`` and ``m + n == 100``. Now it should almost always fail with +``m=0, n=100``. + +This is a relatively niche specialisation, but can be useful in situations +where e.g. a bug is triggered by an integer overflow. From 5e44109f0a2fd55d372304675aeafb453d560eb6 Mon Sep 17 00:00:00 2001 From: "David R. MacIver" Date: Wed, 18 Jul 2018 16:23:34 +0100 Subject: [PATCH 3/3] Guard against shrinking from m=0 --- hypothesis-python/src/hypothesis/internal/conjecture/engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/engine.py b/hypothesis-python/src/hypothesis/internal/conjecture/engine.py index 527b801b7e..bdd5ca84a4 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/engine.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/engine.py @@ -2325,7 +2325,7 @@ def trial(x, y): except OverflowError: return False return self.incorporate_new_buffer(attempt) - if trial(m - 1, n + 1): + if trial(m - 1, n + 1) and m > 1: m = int_from_bytes(self.shrink_target.buffer[u:v]) n = int_from_bytes(self.shrink_target.buffer[r:s])