diff --git a/.travis.yml b/.travis.yml index c37a900..4e792ce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,23 +1,14 @@ language: python python: -- 3.5.0 -- 3.5.2 -- 3.5-dev +- 3.5 +- pypy3.5 - 3.6 -- 3.6-dev -- 3.7-dev +- 3.7 +- 3.8-dev sudo: false -dist: trusty +dist: xenial matrix: include: - - os: linux - language: generic - env: USE_PYPY_RELEASE_VERSION=5.9-beta - # Python3.5 on MacOS doesn't currently support TLSv1.2 needed to use pip :( - # http://pyfound.blogspot.fr/2017/01/time-to-upgrade-your-python-tls-v12.html - # - os: osx - # language: generic - # env: MACPYTHON=3.5.4 - os: osx language: generic env: MACPYTHON=3.6.3 diff --git a/hypothesis_trio/_tests/test_async_stateful.py b/hypothesis_trio/_tests/test_async_stateful.py index f505d17..2869ea2 100644 --- a/hypothesis_trio/_tests/test_async_stateful.py +++ b/hypothesis_trio/_tests/test_async_stateful.py @@ -3,47 +3,11 @@ from trio.abc import Instrument from trio.testing import MockClock -from hypothesis_trio.stateful import TrioGenericStateMachine, TrioRuleBasedStateMachine -from hypothesis.stateful import initialize, rule, invariant, run_state_machine_as_test -from hypothesis.strategies import just, integers, lists, tuples - - -def test_triggers(): - class LogEventsStateMachine(TrioGenericStateMachine): - events = [] - - async def teardown(self): - await trio.sleep(0) - self.events.append('teardown') - - def steps(self): - return just(42) - - async def execute_step(self, step): - await trio.sleep(0) - assert step is 42 - self.events.append('execute_step') - - async def check_invariants(self): - await trio.sleep(0) - self.events.append('check_invariants') - - run_state_machine_as_test(LogEventsStateMachine) - - per_run_events = [] - current_run_events = [] - for event in LogEventsStateMachine.events: - current_run_events.append(event) - if event == 'teardown': - per_run_events.append(current_run_events) - current_run_events = [] - - for run_events in per_run_events: - expected_events = ['check_invariants'] - expected_events += ['execute_step', 'check_invariants' - ] * ((len(run_events) - 2) // 2) - expected_events.append('teardown') - assert run_events == expected_events +import hypothesis +from hypothesis import Verbosity +from hypothesis_trio.stateful import TrioRuleBasedStateMachine +from hypothesis.stateful import Bundle, initialize, rule, invariant, run_state_machine_as_test +from hypothesis.strategies import integers, lists, tuples def test_rule_based(): @@ -150,46 +114,92 @@ async def teardown(self): def test_trio_style(): - async def _consumer( - in_queue, out_queue, *, task_status=trio.TASK_STATUS_IGNORED + async def consumer( + receive_job, send_result, *, task_status=trio.TASK_STATUS_IGNORED ): with trio.open_cancel_scope() as cancel_scope: task_status.started(cancel_scope) - while True: - x, y = await in_queue.get() + async for x, y in receive_job: await trio.sleep(0) result = x + y - await out_queue.put('%s + %s = %s' % (x, y, result)) + await send_result.send('%s + %s = %s' % (x, y, result)) class TrioStyleStateMachine(TrioRuleBasedStateMachine): @initialize() async def initialize(self): - self.job_queue = trio.Queue(100) - self.result_queue = trio.Queue(100) - self.consumer_cancel_scope = await self.get_root_nursery().start( - _consumer, self.job_queue, self.result_queue - ) + self.send_job, receive_job = trio.open_memory_channel(100) + send_result, self.receive_result = trio.open_memory_channel(100) + self.consumer_args = consumer, receive_job, send_result + self.consumer_cancel_scope = await self.get_root_nursery( + ).start(*self.consumer_args) @rule(work=lists(tuples(integers(), integers()))) async def generate_work(self, work): await trio.sleep(0) for x, y in work: - await self.job_queue.put((x, y)) + await self.send_job.send((x, y)) @rule() async def restart_consumer(self): self.consumer_cancel_scope.cancel() - self.consumer_cancel_scope = await self.get_root_nursery().start( - _consumer, self.job_queue, self.result_queue - ) + self.consumer_cancel_scope = await self.get_root_nursery( + ).start(*self.consumer_args) @invariant() async def check_results(self): while True: try: - job = self.result_queue.get_nowait() + job = self.receive_result.receive_nowait() assert isinstance(job, str) except (trio.WouldBlock, AttributeError): break run_state_machine_as_test(TrioStyleStateMachine) + + +def test_trio_style_failing(capsys): + + # Failing state machine + + class TrioStyleStateMachine(TrioRuleBasedStateMachine): + Values = Bundle('value') + + @initialize(target=Values) + async def initialize(self): + return 1 + + @rule(value=Values) + async def do_work(self, value): + assert value == 2 + + # Check failure + + settings = hypothesis.settings(max_examples=10, verbosity=Verbosity.debug) + with pytest.raises(AssertionError) as record: + run_state_machine_as_test(TrioStyleStateMachine, settings=settings) + captured = capsys.readouterr() + assert 'assert 1 == 2' in str(record.value) + + # Check steps + + with pytest.raises(AssertionError) as record: + state = TrioStyleStateMachine() + + async def steps(): + v1 = await state.initialize() + await state.do_work(value=v1) + await state.teardown() + + state.trio_run(steps) + assert 'assert 1 == 2' in str(record.value) + + # Check steps printout + + assert """\ +state = TrioStyleStateMachine() +async def steps(): + v1 = await state.initialize() + await state.do_work(value=v1) + await state.teardown() +state.trio_run(steps) +""" in captured.out diff --git a/hypothesis_trio/_version.py b/hypothesis_trio/_version.py index 0a92cf7..f879b44 100644 --- a/hypothesis_trio/_version.py +++ b/hypothesis_trio/_version.py @@ -1,3 +1,3 @@ # This file is imported from __init__.py and exec'd from setup.py -__version__ = "0.2.2" +__version__ = "0.3.0" diff --git a/hypothesis_trio/stateful.py b/hypothesis_trio/stateful.py index 9b949b0..e57588b 100644 --- a/hypothesis_trio/stateful.py +++ b/hypothesis_trio/stateful.py @@ -1,63 +1,85 @@ import trio from trio.testing import trio_test -import hypothesis.internal.conjecture.utils as cu -from hypothesis._settings import Verbosity -from hypothesis.reporting import current_verbosity + +import hypothesis + from hypothesis.stateful import ( - VarReference, + # Needed for run_state_machine_as_test copy-paste + check_type, + cu, + current_verbosity, + Verbosity, + Settings, + HealthCheck, GenericStateMachine, - StateMachineRunner, - run_state_machine_as_test, - Bundle, - rule, - initialize, - precondition, - invariant, + given, + st, + current_build_context, + function_digest, + # Needed for TrioRuleBasedStateMachine RuleBasedStateMachine, + VarReference, + MultipleResults, + report, ) +# This is an ugly copy-paste since it's currently no possible to plug a special +# runner into run_state_machine_as_test -def monkey_patch_hypothesis(): - def run(self, state_machine, print_steps=None): - if print_steps is None: - print_steps = current_verbosity() >= Verbosity.debug - self.data.hypothesis_runner = state_machine +def run_custom_state_machine_as_test(state_machine_factory, settings=None): + if settings is None: + try: + settings = state_machine_factory.TestCase.settings + check_type( + Settings, settings, "state_machine_factory.TestCase.settings" + ) + except AttributeError: + settings = Settings( + deadline=None, suppress_health_check=HealthCheck.all() + ) + check_type(Settings, settings, "settings") + + @settings + @given(st.data()) + def run_state_machine(factory, data): + machine = factory() + check_type(GenericStateMachine, machine, "state_machine_factory()") + data.conjecture_data.hypothesis_runner = machine + + n_steps = settings.stateful_step_count should_continue = cu.many( - self.data, + data.conjecture_data, min_size=1, - max_size=self.n_steps, - average_size=self.n_steps, + max_size=n_steps, + average_size=n_steps ) - def _default_runner(data, print_steps, should_continue): - try: - if print_steps: - state_machine.print_start() - state_machine.check_invariants() - - while should_continue.more(): - value = data.draw(state_machine.steps()) - if print_steps: - state_machine.print_step(value) - state_machine.execute_step(value) - state_machine.check_invariants() - finally: - if print_steps: - state_machine.print_end() - state_machine.teardown() - - runner = getattr(state_machine, '_custom_runner', _default_runner) - runner(self.data, print_steps, should_continue) + print_steps = ( + current_build_context().is_final + or current_verbosity() >= Verbosity.debug + ) - StateMachineRunner.run = run + return machine._custom_runner(data, print_steps, should_continue) + # Use a machine digest to identify stateful tests in the example database + run_state_machine.hypothesis.inner_test._hypothesis_internal_add_digest = function_digest( + state_machine_factory + ) + # Copy some attributes so @seed and @reproduce_failure "just work" + run_state_machine._hypothesis_internal_use_seed = getattr( + state_machine_factory, "_hypothesis_internal_use_seed", None + ) + run_state_machine._hypothesis_internal_use_reproduce_failure = getattr( + state_machine_factory, "_hypothesis_internal_use_reproduce_failure", + None + ) -monkey_patch_hypothesis() + run_state_machine(state_machine_factory) -class TrioGenericStateMachine(GenericStateMachine): - """Trio compatible version of `hypothesis.stateful.GenericStateMachine` +class TrioRuleBasedStateMachine(RuleBasedStateMachine): + """Trio compatible version of `hypothesis.stateful.RuleBasedStateMachine`. """ def __init__(self): @@ -93,25 +115,33 @@ def push_instrument(self, instrument): raise RuntimeError('Can only add instrument during `__init__`') self.__instruments.append(instrument) + # Runner logic + def _custom_runner(self, data, print_steps, should_continue): + async def runner(machine): + try: + if print_steps: + machine.print_start() + await machine.check_invariants() + + while should_continue.more(): + value = data.conjecture_data.draw(machine.steps()) + if print_steps: + machine.print_step(value) + await machine.execute_step(value) + await machine.check_invariants() + finally: + if print_steps: + self.print_end() + await self.teardown() + + self.trio_run(runner, self) + + def trio_run(self, corofn, *args): async def _run(**kwargs): async with trio.open_nursery() as self._nursery: - try: - if print_steps: - self.print_start() - await self.check_invariants() - - while should_continue.more(): - value = data.draw(self.steps()) - if print_steps: - self.print_step(value) - await self.execute_step(value) - await self.check_invariants() - finally: - if print_steps: - self.print_end() - await self.teardown() - self._nursery.cancel_scope.cancel() + await corofn(*args) + self._nursery.cancel_scope.cancel() self.__started = True kwargs = { @@ -122,9 +152,7 @@ async def _run(**kwargs): kwargs['clock'] = self.__clock trio_test(_run)(**kwargs) - async def execute_step(self, step): - """Execute a step that has been previously drawn from self.steps()""" - raise NotImplementedError(u'%r.execute_step()' % (self,)) + # Async methods async def teardown(self): """Called after a run has finished executing to clean up any necessary @@ -134,16 +162,6 @@ async def teardown(self): """ pass - async def check_invariants(self): - """Called after initializing and after executing each step.""" - pass - - -class TrioRuleBasedStateMachine(TrioGenericStateMachine, - RuleBasedStateMachine): - """Trio compatible version of `hypothesis.stateful.RuleBasedStateMachine`. - """ - async def execute_step(self, step): rule, data = step data = dict(data) @@ -152,14 +170,11 @@ async def execute_step(self, step): data[k] = self.names_to_values[v.name] result = await rule.function(self, **data) if rule.targets: - name = self.new_name() - self.names_to_values[name] = result - # TODO: not really elegant to access __printer this way... - self._RuleBasedStateMachine__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) @@ -169,18 +184,56 @@ async def check_invariants(self): continue await invar.function(self) + # Reporting -__all__ = ( - 'VarReference', - 'GenericStateMachine', - 'StateMachineRunner', - 'run_state_machine_as_test', - 'Bundle', - 'rule', - 'initialize', - 'precondition', - 'invariant', - 'RuleBasedStateMachine', - 'TrioGenericStateMachine', - 'TrioRuleBasedStateMachine', -) + def print_start(self): + report("state = %s()" % (self.__class__.__name__,)) + report("async def steps():") + + def print_end(self): + report(" await state.teardown()") + report("state.trio_run(steps)") + + def print_step(self, step): + rule, data = step + data_repr = {} + for k, v in data.items(): + data_repr[k] = self._RuleBasedStateMachine__pretty(v) + self.step_count = getattr(self, "step_count", 0) + 1 + report( + " %sawait state.%s(%s)" % ( + "%s = " % (self.upcoming_name(),) if rule.targets else "", + rule.function.__name__, + ", ".join("%s=%s" % kv for kv in data_repr.items()), + ) + ) + + +# Monkey patching + + +def monkey_patch_hypothesis(): + if hasattr(hypothesis.stateful, "original_run_state_machine_as_test"): + return + original = hypothesis.stateful.run_state_machine_as_test + + def run_state_machine_as_test(state_machine_factory, settings=None): + """Run a state machine definition as a test, either silently doing nothing + or printing a minimal breaking program and raising an exception. + state_machine_factory is anything which returns an instance of + GenericStateMachine when called with no arguments - it can be a class or a + function. settings will be used to control the execution of the test. + """ + if hasattr(state_machine_factory, '_custom_runner'): + return run_custom_state_machine_as_test( + state_machine_factory, settings=settings + ) + return original(state_machine_factory, settings=settings) + + hypothesis.stateful.original_run_state_machine_as_test = original + hypothesis.stateful.run_state_machine_as_test = run_state_machine_as_test + + +# Monkey patch and expose all objects from original stateful module +monkey_patch_hypothesis() +from hypothesis.stateful import * diff --git a/setup.py b/setup.py index 4832df8..82b27ad 100644 --- a/setup.py +++ b/setup.py @@ -14,9 +14,7 @@ author_email="emmanuel.leblond@gmail.com", license="MIT -or- Apache License 2.0", packages=find_packages(), - install_requires=[ - "trio", - ], + install_requires=["trio", "hypothesis~=4.0"], keywords=[ 'async', 'hypothesis',