Skip to content

Commit

Permalink
Make stateful assign to multiple variables when printing steps… (#2145)
Browse files Browse the repository at this point in the history
Make stateful assign to multiple variables when printing steps that return MultipleResults
  • Loading branch information
Zac-HD committed Nov 2, 2019
2 parents 9c2a715 + 035f57e commit fa88996
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 9 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ their individual contributions.
* `J.J. Green <http://soliton.vm.bytemark.co.uk/pub/jjg/>`_
* `JP Viljoen <https://github.com/froztbyte>`_ (froztbyte@froztbyte.net)
* `Jochen Müller <https://github.com/jomuel>`_
* `Joseph Weston <https://github.com/jbweston>`_
* `Joey Tuong <https://github.com/tetrapus>`_
* `Jonathan Gayvallet <https://github.com/Meallia>`_ (jonathan.gayvallet@orange.com)
* `Jonty Wareing <https://www.github.com/Jonty>`_ (jonty@jonty.co.uk)
Expand Down
5 changes: 5 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
RELEASE_TYPE: patch

This patch makes stateful step printing expand the result of a step into
multiple variables when a MultipleResult is returned (:issue:`2139`).
Thanks to Joseph Weston for reporting and fixing this bug!
45 changes: 36 additions & 9 deletions hypothesis-python/src/hypothesis/stateful.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from hypothesis.control import current_build_context
from hypothesis.core import given
from hypothesis.errors import InvalidArgument, InvalidDefinition
from hypothesis.internal.compat import quiet_raise, string_types
from hypothesis.internal.compat import hrange, quiet_raise, string_types
from hypothesis.internal.reflection import function_digest, nicerepr, proxies, qualname
from hypothesis.internal.validation import check_type
from hypothesis.reporting import current_verbosity, report
Expand Down Expand Up @@ -117,9 +117,16 @@ def run_state_machine(factory, data):

while should_continue.more():
value = data.conjecture_data.draw(machine.steps())
if print_steps:
machine.print_step(value)
machine.execute_step(value)
# Assign 'result' here in case 'execute_step' fails below
result = multiple()
try:
result = machine.execute_step(value)
finally:
if print_steps:
# 'result' is only used if the step has target bundles.
# If it does, and the result is a 'MultipleResult',
# then 'print_step' prints a multi-variable assignment.
machine.print_step(value, result)
machine.check_invariants()
finally:
if print_steps:
Expand Down Expand Up @@ -190,7 +197,10 @@ def steps(self):
raise NotImplementedError(u"%r.steps()" % (self,))

def execute_step(self, step):
"""Execute a step that has been previously drawn from self.steps()"""
"""Execute a step that has been previously drawn from self.steps()
Returns the result of the step execution.
"""
raise NotImplementedError(u"%r.execute_step()" % (self,))

def print_start(self):
Expand All @@ -205,10 +215,10 @@ def print_end(self):
By default does nothing.
"""

def print_step(self, step):
def print_step(self, step, result):
"""Print a step to the current reporter.
This is called right before a step is executed.
This is called right after a step is executed.
"""
self.step_count = getattr(self, u"step_count", 0) + 1
report(u"Step #%d: %s" % (self.step_count, nicerepr(step)))
Expand Down Expand Up @@ -682,6 +692,11 @@ def __repr__(self):
def upcoming_name(self):
return u"v%d" % (self.name_counter,)

def last_names(self, n):
assert self.name_counter > n
count = self.name_counter
return [u"v%d" % (i,) for i in hrange(count - n, count)]

def new_name(self):
result = self.upcoming_name()
self.name_counter += 1
Expand Down Expand Up @@ -777,16 +792,27 @@ def print_start(self):
def print_end(self):
report(u"state.teardown()")

def print_step(self, step):
def print_step(self, step, result):
rule, data = step
data_repr = {}
for k, v in data.items():
data_repr[k] = self.__pretty(v)
self.step_count = getattr(self, u"step_count", 0) + 1
# If the step has target bundles, and the result is a MultipleResults
# then we want to assign to multiple variables.
if isinstance(result, MultipleResults):
n_output_vars = len(result.values)
else:
n_output_vars = 1
output_assignment = (
u"%s = " % (", ".join(self.last_names(n_output_vars)),)
if rule.targets and n_output_vars >= 1
else u""
)
report(
u"%sstate.%s(%s)"
% (
u"%s = " % (self.upcoming_name(),) if rule.targets else u"",
output_assignment,
rule.function.__name__,
u", ".join(u"%s=%s" % kv for kv in data_repr.items()),
)
Expand Down Expand Up @@ -816,6 +842,7 @@ def execute_step(self, step):
self._add_result_to_targets(rule.targets, result)
if self._initialize_rules_to_run:
self._initialize_rules_to_run.remove(rule)
return result

def check_invariants(self):
for invar in self.invariants():
Expand Down
54 changes: 54 additions & 0 deletions hypothesis-python/tests/cover/test_stateful.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,60 @@ def do_not_populate(self):
TestMachineUsingMultiple = MachineUsingMultiple.TestCase


def test_multiple_variables_printed():
class ProducesMultiple(RuleBasedStateMachine):
b = Bundle("b")

@initialize(target=b)
def populate_bundle(self):
return multiple(1, 2)

@rule()
def fail_fast(self):
assert False

with capture_out() as o:
# The state machine must raise an exception for the
# falsifying example to be printed.
with raises(AssertionError):
run_state_machine_as_test(ProducesMultiple)

# This is tightly coupled to the output format of the step printing.
# The first line is "Falsifying Example:..." the second is creating
# the state machine, the third is calling the "initialize" method.
assignment_line = o.getvalue().split("\n")[2]
# 'populate_bundle()' returns 2 values, so should be
# expanded to 2 variables.
assert assignment_line == "v1, v2 = state.populate_bundle()"


def test_no_variables_printed():
class ProducesNoVariables(RuleBasedStateMachine):
b = Bundle("b")

@initialize(target=b)
def populate_bundle(self):
return multiple()

@rule()
def fail_fast(self):
assert False

with capture_out() as o:
# The state machine must raise an exception for the
# falsifying example to be printed.
with raises(AssertionError):
run_state_machine_as_test(ProducesNoVariables)

# This is tightly coupled to the output format of the step printing.
# The first line is "Falsifying Example:..." the second is creating
# the state machine, the third is calling the "initialize" method.
assignment_line = o.getvalue().split("\n")[2]
# 'populate_bundle()' returns 0 values, so there should be no
# variable assignment.
assert assignment_line == "state.populate_bundle()"


def test_consumes_typecheck():
with pytest.raises(TypeError):
consumes(integers())
Expand Down

0 comments on commit fa88996

Please sign in to comment.