Skip to content

Commit

Permalink
Print original bundle variable names in stateful mode
Browse files Browse the repository at this point in the history
  • Loading branch information
Nikita Fomichev committed Feb 29, 2024
1 parent 1be5544 commit e2a9665
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 25 deletions.
4 changes: 4 additions & 0 deletions hypothesis-python/RELEASE.rst
@@ -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
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):
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
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()
)

0 comments on commit e2a9665

Please sign in to comment.