Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
RELEASE_TYPE: patch

This patch adjusts the printing of bundle values to correspond
with their names when using stateful testing.
33 changes: 19 additions & 14 deletions hypothesis-python/src/hypothesis/stateful.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
Notably, the set of steps available at any point may depend on the
execution to date.
"""

import collections
import inspect
from copy import copy
from functools import lru_cache
Expand Down Expand Up @@ -268,7 +268,8 @@ def __init__(self) -> None:
if not self.rules():
raise InvalidDefinition(f"Type {type(self).__name__} defines no rules")
self.bundles: Dict[str, list] = {}
self.name_counter = 1
self.names_counters: collections.Counter = collections.Counter()
self.names_list: list[str] = []
self.names_to_values: Dict[str, Any] = {}
self.__stream = StringIO()
self.__printer = RepresentationPrinter(
Expand Down Expand Up @@ -301,15 +302,16 @@ def _pretty_print(self, value):
def __repr__(self):
return f"{type(self).__name__}({nicerepr(self.bundles)})"

def _new_name(self):
result = f"v{self.name_counter}"
self.name_counter += 1
def _new_name(self, target):
result = f"{target}_{self.names_counters[target]}"
self.names_counters[target] += 1
self.names_list.append(result)
return result

def _last_names(self, n):
assert self.name_counter > n
count = self.name_counter
return [f"v{i}" for i in range(count - n, count)]
len_ = len(self.names_list)
assert len_ >= n
return self.names_list[len_ - n :]

def bundle(self, name):
return self.bundles.setdefault(name, [])
Expand Down Expand Up @@ -364,20 +366,23 @@ def _repr_step(self, rule, data, result):
if len(result.values) == 1:
output_assignment = f"({self._last_names(1)[0]},) = "
elif result.values:
output_names = self._last_names(len(result.values))
number_of_last_names = len(rule.targets) * len(result.values)
output_names = self._last_names(number_of_last_names)
output_assignment = ", ".join(output_names) + " = "
else:
output_assignment = self._last_names(1)[0] + " = "
args = ", ".join("%s=%s" % kv for kv in data.items())
return f"{output_assignment}state.{rule.function.__name__}({args})"

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:
name = self._new_name(target)

def printer(obj, p, cycle, name=name):
return p.text(name)

self.__printer.singleton_pprinters.setdefault(id(result), printer)
self.names_to_values[name] = result
self.bundles.setdefault(target, []).append(VarReference(name))

def check_invariants(self, settings, output, runtimes):
Expand Down
128 changes: 117 additions & 11 deletions hypothesis-python/tests/cover/test_stateful.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,12 @@ def fail_fast(self):
assignment_line = err.value.__notes__[2]
# 'populate_bundle()' returns 2 values, so should be
# expanded to 2 variables.
assert assignment_line == "v1, v2 = state.populate_bundle()"
assert assignment_line == "b_0, b_1 = state.populate_bundle()"

# Make sure MultipleResult is iterable so the printed code is valid.
# See https://github.com/HypothesisWorks/hypothesis/issues/2311
state = ProducesMultiple()
v1, v2 = state.populate_bundle()
b_0, b_1 = state.populate_bundle()
with raises(AssertionError):
state.fail_fast()

Expand All @@ -252,7 +252,7 @@ def fail_fast(self, b):
run_state_machine_as_test(ProducesMultiple)

assignment_line = err.value.__notes__[2]
assert assignment_line == "(v1,) = state.populate_bundle()"
assert assignment_line == "(b_0,) = state.populate_bundle()"

state = ProducesMultiple()
(v1,) = state.populate_bundle()
Expand Down Expand Up @@ -797,9 +797,9 @@ def fail(self, source):
result = "\n".join(err.value.__notes__)
for m in ["create", "transfer", "fail"]:
assert result.count("state." + m) == 1
assert "v1 = state.create()" in result
assert "v2 = state.transfer(source=v1)" in result
assert "state.fail(source=v2)" in result
assert "b1_0 = state.create()" in result
assert "b2_0 = state.transfer(source=b1_0)" in result
assert "state.fail(source=b2_0)" in result


def test_initialize_rule():
Expand Down Expand Up @@ -845,7 +845,7 @@ class WithInitializeBundleRules(RuleBasedStateMachine):

@initialize(target=a, dep=just("dep"))
def initialize_a(self, dep):
return f"a v1 with ({dep})"
return f"a a_0 with ({dep})"

@rule(param=a)
def fail_fast(self, param):
Expand All @@ -861,8 +861,8 @@ def fail_fast(self, param):
== """
Falsifying example:
state = WithInitializeBundleRules()
v1 = state.initialize_a(dep='dep')
state.fail_fast(param=v1)
a_0 = state.initialize_a(dep='dep')
state.fail_fast(param=a_0)
state.teardown()
""".strip()
)
Expand Down Expand Up @@ -1087,8 +1087,8 @@ def mostly_fails(self, d):

with pytest.raises(AssertionError) as err:
run_state_machine_as_test(TrickyPrintingMachine)
assert "v1 = state.init_data(value=0)" in err.value.__notes__
assert "v1 = state.init_data(value=v1)" not in err.value.__notes__
assert "data_0 = state.init_data(value=0)" in err.value.__notes__
assert "data_0 = state.init_data(value=data_0)" not in err.value.__notes__


class TrickyInitMachine(RuleBasedStateMachine):
Expand Down Expand Up @@ -1182,3 +1182,109 @@ def test_fails_on_settings_class_attribute():
match="Assigning .+ as a class attribute does nothing",
):
run_state_machine_as_test(ErrorsOnClassAttributeSettings)


def test_single_target_multiple():
class Machine(RuleBasedStateMachine):
a = Bundle("a")

@initialize(target=a)
def initialize(self):
return multiple("ret1", "ret2", "ret3")

@rule(param=a)
def fail_fast(self, param):
raise AssertionError

Machine.TestCase.settings = NO_BLOB_SETTINGS
with pytest.raises(AssertionError) as err:
run_state_machine_as_test(Machine)

result = "\n".join(err.value.__notes__)
assert (
result
== """
Falsifying example:
state = Machine()
a_0, a_1, a_2 = state.initialize()
state.fail_fast(param=a_2)
state.teardown()
""".strip()
)


def test_multiple_targets():
class Machine(RuleBasedStateMachine):
a = Bundle("a")
b = Bundle("b")

@initialize(targets=(a, b))
def initialize(self):
return multiple("ret1", "ret2", "ret3")

@rule(
a1=consumes(a),
a2=consumes(a),
a3=consumes(a),
b1=consumes(b),
b2=consumes(b),
b3=consumes(b),
)
def fail_fast(self, a1, a2, a3, b1, b2, b3):
raise AssertionError

Machine.TestCase.settings = NO_BLOB_SETTINGS
with pytest.raises(AssertionError) as err:
run_state_machine_as_test(Machine)

result = "\n".join(err.value.__notes__)
assert (
result
== """
Falsifying example:
state = Machine()
a_0, b_0, a_1, b_1, a_2, b_2 = state.initialize()
state.fail_fast(a1=a_2, a2=a_1, a3=a_0, b1=b_2, b2=b_1, b3=b_0)
state.teardown()
""".strip()
)


def test_multiple_common_targets():
class Machine(RuleBasedStateMachine):
a = Bundle("a")
b = Bundle("b")

@initialize(targets=(a, b, a))
def initialize(self):
return multiple("ret1", "ret2", "ret3")

@rule(
a1=consumes(a),
a2=consumes(a),
a3=consumes(a),
a4=consumes(a),
a5=consumes(a),
a6=consumes(a),
b1=consumes(b),
b2=consumes(b),
b3=consumes(b),
)
def fail_fast(self, a1, a2, a3, a4, a5, a6, b1, b2, b3):
raise AssertionError

Machine.TestCase.settings = NO_BLOB_SETTINGS
with pytest.raises(AssertionError) as err:
run_state_machine_as_test(Machine)

result = "\n".join(err.value.__notes__)
assert (
result
== """
Falsifying example:
state = Machine()
a_0, b_0, a_1, a_2, b_1, a_3, a_4, b_2, a_5 = state.initialize()
state.fail_fast(a1=a_5, a2=a_4, a3=a_3, a4=a_2, a5=a_1, a6=a_0, b1=b_2, b2=b_1, b3=b_0)
state.teardown()
""".strip()
)