Repository: HypothesisWorks/hypothesis

Issue:
Stateful: add multiple items to a bundle
I'd like to have steps in my stateful tests that depend on previous steps; IIUC bundles are a way to achieve that.

The problem is, puttings items into a bundle works via `target=bundle` in `@rule`, and I'd like to potentially generate several values per rule execution.

Edit to clarify: I'd like to have subsequent steps in my tests use values generated by prior steps, I'm aware of `@precondition`.



Discussion:
Come to think of it, having a way of prepopulating bundles (from `__init__`) would be great too. 

It's a good idea, and I'm up for implementing it at some point. I think the API and how printing is handled require a bit of a rework to make it possible, but it's reasonably feasible from an internals point of view.

There are a bunch of higher priority things right now which mean improvements to stateful testing aren't really on the short term road map, so it probably won't happen soon though.

> Come to think of it, having a way of prepopulating bundles (from `__init__`) would be great too.

This is now covered by `@initialize` rules, which are run exactly once each prior to any other rule.
Do we already have a plan what the API for this should look like? My proposal would be 

- a function to wrap the results when returning, like: ``return multiple(result1, result2, result3)``
- since ``multiple()`` could be used to add no result to the bundle, it should have a convenient alias like ``return no_result()``.

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
@@ -331,6 +331,21 @@ def consumes(bundle):
     return BundleConsumer(bundle)
 
 
+@attr.s()
+class MultipleResults(object):
+    values = attr.ib()
+
+
+def multiple(*args):
+    """This function can be used to pass multiple results to the target(s) of
+    a rule. Just use ``return multiple(result1, result2, ...)`` in your rule.
+
+    It is also possible to use ``return multiple()`` with no arguments in
+    order to end a rule without passing any result.
+    """
+    return MultipleResults(args)
+
+
 def _convert_targets(targets, target):
     """Single validator and convertor for target arguments."""
     if target is not None:
@@ -795,6 +810,15 @@ def print_step(self, step):
             )
         )
 
+    def _add_result_to_targets(self, targets, result):
+        name = self.new_name()
+        self.__printer.singleton_pprinters.setdefault(
+            id(result), lambda obj, p, cycle: p.text(name)
+        )
+        self.names_to_values[name] = result
+        for target in targets:
+            self.bundle(target).append(VarReference(name))
+
     def execute_step(self, step):
         rule, data = step
         data = dict(data)
@@ -803,13 +827,11 @@ def execute_step(self, step):
                 data[k] = self.names_to_values[v.name]
         result = rule.function(self, **data)
         if rule.targets:
-            name = self.new_name()
-            self.names_to_values[name] = result
-            self.__printer.singleton_pprinters.setdefault(
-                id(result), lambda obj, p, cycle: p.text(name)
-            )
-            for target in rule.targets:
-                self.bundle(target).append(VarReference(name))
+            if isinstance(result, MultipleResults):
+                for single_result in result.values:
+                    self._add_result_to_targets(rule.targets, single_result)
+            else:
+                self._add_result_to_targets(rule.targets, result)
         if self._initialize_rules_to_run:
             self._initialize_rules_to_run.remove(rule)
 


Test patch:
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
@@ -42,6 +42,7 @@
     consumes,
     initialize,
     invariant,
+    multiple,
     precondition,
     rule,
     run_state_machine_as_test,
@@ -388,6 +389,37 @@ def check(self, value1, value2):
 TestMachineWithConsumingRule = MachineWithConsumingRule.TestCase
 
 
+def test_multiple():
+    none = multiple()
+    some = multiple(1, 2.01, "3", b"4", 5)
+    assert len(none.values) == 0 and len(some.values) == 5
+    assert all(value in some.values for value in (1, 2.01, "3", b"4", 5))
+
+
+class MachineUsingMultiple(RuleBasedStateMachine):
+    b = Bundle("b")
+
+    def __init__(self):
+        self.expected_bundle_length = 0
+        super(MachineUsingMultiple, self).__init__()
+
+    @invariant()
+    def bundle_length(self):
+        assert len(self.bundle("b")) == self.expected_bundle_length
+
+    @rule(target=b, items=lists(elements=integers(), max_size=10))
+    def populate_bundle(self, items):
+        self.expected_bundle_length += len(items)
+        return multiple(*items)
+
+    @rule(target=b)
+    def do_not_populate(self):
+        return multiple()
+
+
+TestMachineUsingMultiple = MachineUsingMultiple.TestCase
+
+
 def test_consumes_typecheck():
     with pytest.raises(TypeError):
         consumes(integers())