File: test_slippage.py

package info (click to toggle)
python-hypothesis 6.138.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 15,272 kB
  • sloc: python: 62,853; ruby: 1,107; sh: 253; makefile: 41; javascript: 6
file content (364 lines) | stat: -rw-r--r-- 10,469 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# This file is part of Hypothesis, which may be found at
# https://github.com/HypothesisWorks/hypothesis/
#
# Copyright the Hypothesis Authors.
# Individual contributors are listed in AUTHORS.rst and the git log.
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.

import pytest

from hypothesis import Phase, assume, given, settings, strategies as st, target
from hypothesis.database import InMemoryExampleDatabase
from hypothesis.errors import FlakyFailure
from hypothesis.internal.compat import ExceptionGroup
from hypothesis.internal.conjecture.engine import MIN_TEST_CALLS

from tests.common.utils import (
    Why,
    assert_output_contains_failure,
    capture_out,
    non_covering_examples,
    xfail_on_crosshair,
)


def capture_reports(test):
    with capture_out() as o:
        # NOTE: For compatibility with Python 3.9's LL(1)
        # parser, this is written as a nested with-statement,
        # instead of a compound one.
        with pytest.raises(ExceptionGroup) as err:
            test()

    return o.getvalue() + "\n\n".join(
        f"{e!r}\n" + "\n".join(getattr(e, "__notes__", []))
        for e in (err.value, *err.value.exceptions)
    )


@xfail_on_crosshair(Why.symbolic_outside_context, strict=False)
def test_raises_multiple_failures_with_varying_type():
    target = None

    @settings(database=None, max_examples=100, report_multiple_bugs=True)
    @given(st.integers())
    def test(i):
        nonlocal target
        if abs(i) < 1000:
            return
        if target is None:
            # Ensure that we have some space to shrink into, so we can't
            # trigger an minimal example and mask the other exception type.
            assume(1003 < abs(i))
            target = i
        exc_class = TypeError if target == i else ValueError
        raise exc_class

    output = capture_reports(test)
    assert "TypeError" in output
    assert "ValueError" in output


@pytest.mark.skipif(
    settings().backend != "hypothesis", reason="no multiple failures on backends (yet?)"
)
def test_shows_target_scores_with_multiple_failures():
    @settings(derandomize=True, max_examples=10_000)
    @given(st.integers())
    def test(i):
        target(i)
        assert i > 0
        assert i < 0

    assert "Highest target score:" in capture_reports(test)


@xfail_on_crosshair(Why.symbolic_outside_context, strict=False)
def test_raises_multiple_failures_when_position_varies():
    target = None

    @settings(max_examples=100, report_multiple_bugs=True)
    @given(st.integers())
    def test(i):
        nonlocal target
        if abs(i) < 1000:
            return
        if target is None:
            target = i
        if target == i:
            raise ValueError("loc 1")
        else:
            raise ValueError("loc 2")

    output = capture_reports(test)
    assert "loc 1" in output
    assert "loc 2" in output


@xfail_on_crosshair(Why.symbolic_outside_context, strict=False)
def test_replays_both_failing_values():
    target = None

    @settings(
        database=InMemoryExampleDatabase(), max_examples=500, report_multiple_bugs=True
    )
    @given(st.integers())
    def test(i):
        nonlocal target
        if abs(i) < 1000:
            return
        if target is None:
            target = i
        exc_class = TypeError if target == i else ValueError
        raise exc_class

    with pytest.raises(ExceptionGroup):
        test()

    with pytest.raises(ExceptionGroup):
        test()


@xfail_on_crosshair(Why.symbolic_outside_context, strict=False)
@pytest.mark.parametrize("fix", [TypeError, ValueError])
def test_replays_slipped_examples_once_initial_bug_is_fixed(fix):
    target = []
    bug_fixed = False

    @settings(
        database=InMemoryExampleDatabase(), max_examples=500, report_multiple_bugs=True
    )
    @given(st.integers())
    def test(i):
        if abs(i) < 1000:
            return
        if not target:
            target.append(i)
        if i == target[0]:
            if bug_fixed and fix == TypeError:
                return
            raise TypeError
        if len(target) == 1:
            target.append(i)
        if bug_fixed and fix == ValueError:
            return
        if i == target[1]:
            raise ValueError

    with pytest.raises(ExceptionGroup):
        test()

    bug_fixed = True

    with pytest.raises(ValueError if fix == TypeError else TypeError):
        test()


@xfail_on_crosshair(Why.symbolic_outside_context, strict=False)
def test_garbage_collects_the_secondary_key():
    target = []
    bug_fixed = False

    db = InMemoryExampleDatabase()

    @settings(database=db, max_examples=500, report_multiple_bugs=True)
    @given(st.integers())
    def test(i):
        if bug_fixed:
            return
        if abs(i) < 1000:
            return
        if not target:
            target.append(i)
        if i == target[0]:
            raise TypeError
        if len(target) == 1:
            target.append(i)
        if i == target[1]:
            raise ValueError

    with pytest.raises(ExceptionGroup):
        test()

    bug_fixed = True

    def count():
        return len(non_covering_examples(db))

    prev = count()
    while prev > 0:
        test()
        current = count()
        assert current < prev
        prev = current


def test_shrinks_both_failures():
    first_has_failed = False
    duds = set()
    second_target = None

    @settings(database=None, max_examples=1000, report_multiple_bugs=True)
    @given(st.integers(min_value=0))
    def test(i):
        nonlocal first_has_failed, duds, second_target

        if i >= 10000:
            first_has_failed = True
            raise AssertionError

        assert i < 10000
        if first_has_failed:
            if second_target is None:
                for j in range(10000):
                    if j not in duds:
                        second_target = j
                        break
            # to avoid flaky errors, don't error on an input that we previously
            # passed.
            if i not in duds:
                assert i < second_target
        else:
            duds.add(i)

    output = capture_reports(test)
    assert_output_contains_failure(output, test, i=10000)
    assert_output_contains_failure(output, test, i=second_target)


@xfail_on_crosshair(Why.symbolic_outside_context, strict=False)
def test_handles_flaky_tests_where_only_one_is_flaky():
    flaky_fixed = False

    target = []
    flaky_failed_once = False

    @settings(
        database=InMemoryExampleDatabase(), max_examples=1000, report_multiple_bugs=True
    )
    @given(st.integers())
    def test(i):
        nonlocal flaky_failed_once
        if abs(i) < 1000:
            return
        if not target:
            target.append(i)
        if i == target[0]:
            raise TypeError
        if flaky_failed_once and not flaky_fixed:
            return
        if len(target) == 1:
            target.append(i)
        if i == target[1]:
            flaky_failed_once = True
            raise ValueError

    with pytest.raises(ExceptionGroup) as err:
        test()
    assert any(isinstance(e, FlakyFailure) for e in err.value.exceptions)

    flaky_fixed = True

    with pytest.raises(ExceptionGroup) as err:
        test()
    assert not any(isinstance(e, FlakyFailure) for e in err.value.exceptions)


@pytest.mark.skipif(
    settings().backend != "hypothesis", reason="no multiple failures on backends (yet?)"
)
@pytest.mark.parametrize("allow_multi", [True, False])
def test_can_disable_multiple_error_reporting(allow_multi):
    seen = set()

    @settings(database=None, derandomize=True, report_multiple_bugs=allow_multi)
    @given(st.integers(min_value=0))
    def test(i):
        # We will pass on the minimal i=0, then fail with a large i, and eventually
        # slip to i=1 and a different error.  We check both seen and raised errors.
        if i == 1:
            seen.add(TypeError)
            raise TypeError
        elif i >= 2:
            seen.add(ValueError)
            raise ValueError

    with pytest.raises(ExceptionGroup if allow_multi else TypeError):
        test()
    assert seen == {TypeError, ValueError}


@xfail_on_crosshair(Why.symbolic_outside_context, strict=False)
def test_finds_multiple_failures_in_generation():
    special = None
    seen = set()

    @settings(
        phases=[Phase.generate, Phase.shrink],
        max_examples=100,
        report_multiple_bugs=True,
    )
    @given(st.integers(min_value=0))
    def test(x):
        """Constructs a test so the 10th largeish example we've seen is a
        special failure, and anything new we see after that point that
        is larger than it is a different failure. This demonstrates that we
        can keep generating larger examples and still find new bugs after that
        point."""
        nonlocal special
        if not special:
            # don't mark duplicate inputs as special and thus erroring, to avoid
            # flakiness where we passed the input the first time but failed it the
            # second.
            if len(seen) >= 10 and x <= 1000 and x not in seen:
                special = x
            else:
                seen.add(x)

        if special:
            assert x in seen or x <= special
        assert x != special

    with pytest.raises(ExceptionGroup):
        test()


def test_stops_immediately_if_not_report_multiple_bugs():
    seen = set()

    @settings(phases=[Phase.generate], report_multiple_bugs=False)
    @given(st.integers())
    def test(x):
        seen.add(x)
        raise AssertionError

    with pytest.raises(AssertionError):
        test()
    assert len(seen) == 1


@pytest.mark.skipif(
    settings().backend != "hypothesis", reason="unclear backend semantics"
)
def test_stops_immediately_on_replay():
    seen = set()

    @settings(database=InMemoryExampleDatabase(), phases=tuple(Phase)[:-1])
    @given(st.integers())
    def test(x):
        seen.add(x)
        assert x

    # On the first run, we look for up to ten examples:
    with pytest.raises(AssertionError):
        test()
    assert 1 < len(seen) <= MIN_TEST_CALLS

    # With failing examples in the database, we stop at one.
    seen.clear()
    with pytest.raises(AssertionError):
        test()
    assert len(seen) == 1