|
8 | 8 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can
|
9 | 9 | # obtain one at https://mozilla.org/MPL/2.0/.
|
10 | 10 |
|
| 11 | +import inspect |
11 | 12 | from collections import namedtuple
|
12 | 13 |
|
13 | 14 | import pytest
|
14 | 15 |
|
15 |
| -from hypothesis import settings as Settings |
| 16 | +from hypothesis import Phase, settings as Settings, strategies as st |
16 | 17 | from hypothesis.stateful import (
|
17 | 18 | Bundle,
|
18 | 19 | RuleBasedStateMachine,
|
|
21 | 22 | rule,
|
22 | 23 | run_state_machine_as_test,
|
23 | 24 | )
|
24 |
| -from hypothesis.strategies import booleans, integers, lists |
| 25 | + |
| 26 | + |
| 27 | +def run_to_notes(TestClass): |
| 28 | + TestCase = TestClass.TestCase |
| 29 | + # don't add explain phase notes to the error |
| 30 | + TestCase.settings = Settings(phases=set(Phase) - {Phase.explain}) |
| 31 | + try: |
| 32 | + TestCase().runTest() |
| 33 | + except AssertionError as err: |
| 34 | + return err.__notes__ |
| 35 | + |
| 36 | + raise RuntimeError("Expected an assertion error") |
| 37 | + |
| 38 | + |
| 39 | +def assert_runs_to_output(TestClass, output): |
| 40 | + # remove the first line, which is always "Falsfying example:" |
| 41 | + actual = "\n".join(run_to_notes(TestClass)[1:]) |
| 42 | + assert actual == inspect.cleandoc(output.strip()) |
| 43 | + |
25 | 44 |
|
26 | 45 | Leaf = namedtuple("Leaf", ("label",))
|
27 | 46 | Split = namedtuple("Split", ("left", "right"))
|
|
30 | 49 | class BalancedTrees(RuleBasedStateMachine):
|
31 | 50 | trees = Bundle("BinaryTree")
|
32 | 51 |
|
33 |
| - @rule(target=trees, x=booleans()) |
| 52 | + @rule(target=trees, x=st.booleans()) |
34 | 53 | def leaf(self, x):
|
35 | 54 | return Leaf(x)
|
36 | 55 |
|
@@ -81,7 +100,7 @@ def is_not_too_deep(self, check):
|
81 | 100 | class RoseTreeStateMachine(RuleBasedStateMachine):
|
82 | 101 | nodes = Bundle("nodes")
|
83 | 102 |
|
84 |
| - @rule(target=nodes, source=lists(nodes)) |
| 103 | + @rule(target=nodes, source=st.lists(nodes)) |
85 | 104 | def bunch(self, source):
|
86 | 105 | return source
|
87 | 106 |
|
@@ -149,7 +168,7 @@ def __init__(self):
|
149 | 168 | # achieve "swarming" by by just restricting the alphabet for single byte
|
150 | 169 | # decisions, which is a thing the underlying conjecture engine will
|
151 | 170 | # happily do on its own without knowledge of the rule structure.
|
152 |
| - @rule(move=integers(0, 255)) |
| 171 | + @rule(move=st.integers(0, 255)) |
153 | 172 | def ladder(self, move):
|
154 | 173 | self.seen.add(move)
|
155 | 174 | assert len(self.seen) <= 15
|
@@ -213,29 +232,29 @@ class TestMyStatefulMachine(MyStatefulMachine.TestCase):
|
213 | 232 | def test_multiple_precondition_bug():
|
214 | 233 | # See https://github.com/HypothesisWorks/hypothesis/issues/2861
|
215 | 234 | class MultiplePreconditionMachine(RuleBasedStateMachine):
|
216 |
| - @rule(x=integers()) |
| 235 | + @rule(x=st.integers()) |
217 | 236 | def good_method(self, x):
|
218 | 237 | pass
|
219 | 238 |
|
220 | 239 | @precondition(lambda self: True)
|
221 | 240 | @precondition(lambda self: False)
|
222 |
| - @rule(x=integers()) |
| 241 | + @rule(x=st.integers()) |
223 | 242 | def bad_method_a(self, x):
|
224 | 243 | raise AssertionError("This rule runs, even though it shouldn't.")
|
225 | 244 |
|
226 | 245 | @precondition(lambda self: False)
|
227 | 246 | @precondition(lambda self: True)
|
228 |
| - @rule(x=integers()) |
| 247 | + @rule(x=st.integers()) |
229 | 248 | def bad_method_b(self, x):
|
230 | 249 | raise AssertionError("This rule might be skipped for the wrong reason.")
|
231 | 250 |
|
232 | 251 | @precondition(lambda self: True)
|
233 |
| - @rule(x=integers()) |
| 252 | + @rule(x=st.integers()) |
234 | 253 | @precondition(lambda self: False)
|
235 | 254 | def bad_method_c(self, x):
|
236 | 255 | raise AssertionError("This rule runs, even though it shouldn't.")
|
237 | 256 |
|
238 |
| - @rule(x=integers()) |
| 257 | + @rule(x=st.integers()) |
239 | 258 | @precondition(lambda self: True)
|
240 | 259 | @precondition(lambda self: False)
|
241 | 260 | def bad_method_d(self, x):
|
@@ -266,3 +285,98 @@ def bad_invariant_d(self):
|
266 | 285 | raise AssertionError("This invariant runs, even though it shouldn't.")
|
267 | 286 |
|
268 | 287 | run_state_machine_as_test(MultiplePreconditionMachine)
|
| 288 | + |
| 289 | + |
| 290 | +class UnrelatedCall(RuleBasedStateMachine): |
| 291 | + a = Bundle("a") |
| 292 | + |
| 293 | + def __init__(self): |
| 294 | + super().__init__() |
| 295 | + self.calls = set() |
| 296 | + |
| 297 | + @rule(target=a, a=st.integers()) |
| 298 | + def add_a(self, a): |
| 299 | + self.calls.add("add") |
| 300 | + return a |
| 301 | + |
| 302 | + @rule(v=a) |
| 303 | + def f(self, v): |
| 304 | + self.calls.add("f") |
| 305 | + |
| 306 | + @precondition(lambda self: "add" in self.calls) |
| 307 | + @rule(value=st.integers()) |
| 308 | + def unrelated(self, value): |
| 309 | + self.calls.add("unrelated") |
| 310 | + |
| 311 | + @rule() |
| 312 | + def invariant(self): |
| 313 | + # force all three calls to be made in a particular order (with the |
| 314 | + # `unrelated` precondition) so we always shrink to a particular counterexample. |
| 315 | + assert len(self.calls) != 3 |
| 316 | + |
| 317 | + |
| 318 | +def test_unrelated_rule_does_not_use_var_reference_repr(): |
| 319 | + # we are specifically looking for state.unrelated(value=0) not being replaced |
| 320 | + # with state.unrelated(value=a_0). The `unrelated` rule is drawing from |
| 321 | + # st.integers, not a bundle, so the values should not be conflated even if |
| 322 | + # they're both 0. |
| 323 | + assert_runs_to_output( |
| 324 | + UnrelatedCall, |
| 325 | + """ |
| 326 | + state = UnrelatedCall() |
| 327 | + a_0 = state.add_a(a=0) |
| 328 | + state.f(v=a_0) |
| 329 | + state.unrelated(value=0) |
| 330 | + state.invariant() |
| 331 | + state.teardown() |
| 332 | + """, |
| 333 | + ) |
| 334 | + |
| 335 | + |
| 336 | +class SourceSameAsTarget(RuleBasedStateMachine): |
| 337 | + values = Bundle("values") |
| 338 | + |
| 339 | + @rule(target=values, value=st.lists(values)) |
| 340 | + def f(self, value): |
| 341 | + assert len(value) == 0 |
| 342 | + return value |
| 343 | + |
| 344 | + |
| 345 | +class SourceSameAsTargetUnclearOrigin(RuleBasedStateMachine): |
| 346 | + values = Bundle("values") |
| 347 | + |
| 348 | + def __init__(self): |
| 349 | + super().__init__() |
| 350 | + self.called = False |
| 351 | + |
| 352 | + @rule(target=values, value=st.just([]) | st.lists(values)) |
| 353 | + def f(self, value): |
| 354 | + assert not self.called |
| 355 | + # ensure we get two calls to f before failing. In the minimal failing |
| 356 | + # example, both will be from st.just([]). |
| 357 | + self.called = True |
| 358 | + return value |
| 359 | + |
| 360 | + |
| 361 | +def test_replaces_when_same_id(): |
| 362 | + assert_runs_to_output( |
| 363 | + SourceSameAsTarget, |
| 364 | + f""" |
| 365 | + state = {SourceSameAsTarget.__name__}() |
| 366 | + values_0 = state.f(value=[]) |
| 367 | + state.f(value=[values_0]) |
| 368 | + state.teardown() |
| 369 | + """, |
| 370 | + ) |
| 371 | + |
| 372 | + |
| 373 | +def test_doesnt_replace_when_different_id(): |
| 374 | + assert_runs_to_output( |
| 375 | + SourceSameAsTargetUnclearOrigin, |
| 376 | + f""" |
| 377 | + state = {SourceSameAsTargetUnclearOrigin.__name__}() |
| 378 | + values_0 = state.f(value=[]) |
| 379 | + state.f(value=[]) |
| 380 | + state.teardown() |
| 381 | + """, |
| 382 | + ) |
0 commit comments