Repository: HypothesisWorks/hypothesis

Issue:
Deprecate stateful.GenericStateMachine
After #1609, it's pretty obvious that a `GenericStateMachine` just wraps a loop using `st.data()`.

It used to do more, but with internal upgrades to `st.data()` over 2018, and of course #1609 itself, it's time to retire this and celebrate as the stateful API gets a little simpler.


Discussion:

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/stateful.py b/hypothesis-python/src/hypothesis/stateful.py
--- a/hypothesis-python/src/hypothesis/stateful.py
+++ b/hypothesis-python/src/hypothesis/stateful.py
@@ -89,7 +89,17 @@ def run_state_machine_as_test(state_machine_factory, settings=None):
     @given(st.data())
     def run_state_machine(factory, data):
         machine = factory()
-        check_type(GenericStateMachine, machine, "state_machine_factory()")
+        if isinstance(machine, GenericStateMachine) and not isinstance(
+            machine, RuleBasedStateMachine
+        ):
+            note_deprecation(
+                "%s inherits from GenericStateMachine, which is deprecated.  Use a "
+                "RuleBasedStateMachine, or a test function with st.data(), instead."
+                % (type(machine).__name__,),
+                since="RELEASEDAY",
+            )
+        else:
+            check_type(RuleBasedStateMachine, machine, "state_machine_factory()")
         data.conjecture_data.hypothesis_runner = machine
 
         n_steps = settings.stateful_step_count
@@ -150,25 +160,28 @@ def __setattr__(self, name, value):
 class GenericStateMachine(
     GenericStateMachineMeta("GenericStateMachine", (object,), {})  # type: ignore
 ):
-    """A GenericStateMachine is the basic entry point into Hypothesis's
-    approach to stateful testing.
-
-    The intent is for it to be subclassed to provide state machine descriptions
-
-    The way this is used is that Hypothesis will repeatedly execute something
-    that looks something like::
-
-        x = MyStatemachineSubclass()
-        x.check_invariants()
-        try:
-            for _ in range(n_steps):
-                x.execute_step(x.steps().example())
-                x.check_invariants()
-        finally:
-            x.teardown()
-
-    And if this ever produces an error it will shrink it down to a small
-    sequence of example choices demonstrating that.
+    """A GenericStateMachine is a deprecated approach to stateful testing.
+
+    In earlier versions of Hypothesis, you would define ``steps``,
+    ``execute_step``, ``teardown``, and ``check_invariants`` methods;
+    and the engine would then run something like the following::
+
+        @given(st.data())
+        def test_the_stateful_thing(data):
+            x = MyStatemachineSubclass()
+            x.check_invariants()
+            try:
+                for _ in range(50):
+                    step = data.draw(x.steps())
+                    x.execute_step(step)
+                    x.check_invariants()
+            finally:
+                x.teardown()
+
+    We now recommend using rule-based stateful testing instead wherever
+    possible.  If your test is better expressed in the above format than
+    as a rule-based state machine, we suggest "unrolling" your method
+    definitions into a simple test function with the above control flow.
     """
 
     def steps(self):


Test patch:
diff --git a/hypothesis-python/tests/cover/test_runner_strategy.py b/hypothesis-python/tests/cover/test_runner_strategy.py
--- a/hypothesis-python/tests/cover/test_runner_strategy.py
+++ b/hypothesis-python/tests/cover/test_runner_strategy.py
@@ -23,7 +23,7 @@
 
 from hypothesis import find, given, strategies as st
 from hypothesis.errors import InvalidArgument
-from hypothesis.stateful import GenericStateMachine
+from hypothesis.stateful import RuleBasedStateMachine, rule
 
 
 def test_cannot_use_without_a_runner():
@@ -60,12 +60,10 @@ def test_runner_is_self_even_with_default(self, runner):
         assert runner is self
 
 
-class RunnerStateMachine(GenericStateMachine):
-    def steps(self):
-        return st.runner()
-
-    def execute_step(self, step):
-        assert self is step
+class RunnerStateMachine(RuleBasedStateMachine):
+    @rule(runner=st.runner())
+    def step(self, runner):
+        assert runner is self
 
 
 TestState = RunnerStateMachine.TestCase
diff --git a/hypothesis-python/tests/cover/test_stateful.py b/hypothesis-python/tests/cover/test_stateful.py
--- a/hypothesis-python/tests/cover/test_stateful.py
+++ b/hypothesis-python/tests/cover/test_stateful.py
@@ -26,7 +26,6 @@
 from hypothesis import (
     PrintSettings,
     __version__,
-    assume,
     reproduce_failure,
     seed,
     settings as Settings,
@@ -93,22 +92,6 @@ def execute_step(self, step):
         self.counter = step
 
 
-class GoodSet(GenericStateMachine):
-    def __init__(self):
-        self.stuff = set()
-
-    def steps(self):
-        return tuples(booleans(), integers())
-
-    def execute_step(self, step):
-        delete, value = step
-        if delete:
-            self.stuff.discard(value)
-        else:
-            self.stuff.add(value)
-        assert delete == (value not in self.stuff)
-
-
 Leaf = namedtuple(u"Leaf", (u"label",))
 Split = namedtuple(u"Split", (u"left", u"right"))
 
@@ -247,8 +230,6 @@ def fail(self, x, y):
 
 
 bad_machines = (
-    OrderedStateMachine,
-    SetStateMachine,
     BalancedTrees,
     DepthMachine,
     RoseTreeStateMachine,
@@ -296,17 +277,9 @@ def test_multiple_rules_same_func():
     assert "rule2data" in output
 
 
-class GivenLikeStateMachine(GenericStateMachine):
-    def steps(self):
-        return lists(booleans(), min_size=1)
-
-    def execute_step(self, step):
-        assume(any(step))
-
-
 def test_can_get_test_case_off_machine_instance():
-    assert GoodSet().TestCase is GoodSet().TestCase
-    assert GoodSet().TestCase is not None
+    assert SetStateMachine().TestCase is SetStateMachine().TestCase
+    assert SetStateMachine().TestCase is not None
 
 
 class FlakyDrawLessMachine(GenericStateMachine):
@@ -323,6 +296,7 @@ def execute_step(self, step):
             assert 0 not in bytearray(step)
 
 
+@checks_deprecated_behaviour
 def test_flaky_draw_less_raises_flaky():
     with raises(Flaky):
         FlakyDrawLessMachine.TestCase().runTest()
@@ -336,6 +310,7 @@ def execute_step(self, step):
         assert current_build_context().is_final
 
 
+@checks_deprecated_behaviour
 def test_flaky_raises_flaky():
     with raises(Flaky):
         FlakyStateMachine.TestCase().runTest()
@@ -424,6 +399,7 @@ def test_consumes_typecheck():
         consumes(integers())
 
 
+@checks_deprecated_behaviour
 def test_ratchetting_raises_flaky():
     with raises(Flaky):
         FlakyRatchettingMachine.TestCase().runTest()
@@ -471,20 +447,12 @@ class IntAdder(RuleBasedStateMachine):
 )
 
 
-TestGoodSets = GoodSet.TestCase
-TestGivenLike = GivenLikeStateMachine.TestCase
 TestDynamicMachine = DynamicMachine.TestCase
 TestIntAdder = IntAdder.TestCase
 TestPrecondition = PreconditionMachine.TestCase
 
 
-for test_case in (
-    TestGoodSets,
-    TestGivenLike,
-    TestDynamicMachine,
-    TestIntAdder,
-    TestPrecondition,
-):
+for test_case in (TestDynamicMachine, TestIntAdder, TestPrecondition):
     test_case.settings = Settings(test_case.settings, max_examples=10)
 
 
@@ -502,6 +470,7 @@ class Foo(RuleBasedStateMachine):
     assert len(Foo.rules()) == 2
 
 
+@checks_deprecated_behaviour
 def test_minimizes_errors_in_teardown():
     counter = [0]
 
@@ -536,11 +505,13 @@ def execute_step(self, value):
             raise ValueError(u"%d is too high" % (value,))
 
 
+@checks_deprecated_behaviour
 def test_can_use_factory_for_tests():
     with raises(ValueError):
         run_state_machine_as_test(lambda: RequiresInit(42))
 
 
+@Settings(stateful_step_count=5)
 class FailsEventually(GenericStateMachine):
     def __init__(self):
         super(FailsEventually, self).__init__()
@@ -554,14 +525,9 @@ def execute_step(self, _):
         assert self.counter < 10
 
 
-FailsEventually.TestCase.settings = Settings(
-    FailsEventually.TestCase.settings, stateful_step_count=5
-)
-
-TestDoesNotFail = FailsEventually.TestCase
-
-
+@checks_deprecated_behaviour
 def test_can_explicitly_pass_settings():
+    run_state_machine_as_test(FailsEventually)
     try:
         FailsEventually.TestCase.settings = Settings(
             FailsEventually.TestCase.settings, stateful_step_count=15
@@ -595,6 +561,7 @@ def test_settings_attribute_is_validated():
         FailsEventually.TestCase.settings = real_settings
 
 
+@checks_deprecated_behaviour
 def test_saves_failing_example_in_database():
     db = ExampleDatabase(":memory:")
     with raises(AssertionError):
@@ -602,6 +569,7 @@ def test_saves_failing_example_in_database():
     assert any(list(db.data.values()))
 
 
+@checks_deprecated_behaviour
 def test_can_run_with_no_db():
     with raises(AssertionError):
         run_state_machine_as_test(SetStateMachine, Settings(database=None))
diff --git a/hypothesis-python/tests/pytest/test_statistics.py b/hypothesis-python/tests/pytest/test_statistics.py
--- a/hypothesis-python/tests/pytest/test_statistics.py
+++ b/hypothesis-python/tests/pytest/test_statistics.py
@@ -98,17 +98,11 @@ def test_prints_statistics_for_unittest_tests(testdir):
 
 
 STATEFUL_TESTSUITE = """
+from hypothesis.stateful import RuleBasedStateMachine, rule
 
-from hypothesis import given
-from hypothesis.strategies import integers
-from hypothesis.stateful import GenericStateMachine
-
-
-class Stuff(GenericStateMachine):
-    def steps(self):
-        return integers()
-
-    def execute_step(self, step):
+class Stuff(RuleBasedStateMachine):
+    @rule()
+    def step(self):
         pass
 
 TestStuff = Stuff.TestCase