diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..ac89cc112e --- /dev/null +++ b/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. diff --git a/hypothesis-python/src/hypothesis/stateful.py b/hypothesis-python/src/hypothesis/stateful.py index 60cd92721c..58a2277161 100644 --- a/hypothesis-python/src/hypothesis/stateful.py +++ b/hypothesis-python/src/hypothesis/stateful.py @@ -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 @@ -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( @@ -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, []) @@ -364,7 +366,8 @@ 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] + " = " @@ -372,12 +375,14 @@ def _repr_step(self, rule, data, result): 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): diff --git a/hypothesis-python/tests/cover/test_stateful.py b/hypothesis-python/tests/cover/test_stateful.py index 84b9edcc59..d9c2e09efb 100644 --- a/hypothesis-python/tests/cover/test_stateful.py +++ b/hypothesis-python/tests/cover/test_stateful.py @@ -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() @@ -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() @@ -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(): @@ -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): @@ -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() ) @@ -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): @@ -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() + )