From 956bdb45fad50b1785250777cf666e0ddb90f217 Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Mon, 18 May 2020 18:58:25 +0400 Subject: [PATCH 1/3] feat: contract_strategy --- brownie/test/__init__.py | 2 +- brownie/test/strategies.py | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/brownie/test/__init__.py b/brownie/test/__init__.py index 3f6f42f17..3b3d1001d 100644 --- a/brownie/test/__init__.py +++ b/brownie/test/__init__.py @@ -9,7 +9,7 @@ from brownie.exceptions import BrownieTestWarning from .stateful import state_machine # NOQA: F401 -from .strategies import strategy # NOQA: F401 +from .strategies import contract_strategy, strategy # NOQA: F401 # hypothesis warns against combining function-scoped fixtures with @given # but in brownie this is a documented and useful behaviour diff --git a/brownie/test/strategies.py b/brownie/test/strategies.py index 6298a5d1e..91c85c994 100644 --- a/brownie/test/strategies.py +++ b/brownie/test/strategies.py @@ -7,7 +7,7 @@ from hypothesis.strategies import SearchStrategy from hypothesis.strategies._internal.deferred import DeferredStrategy -from brownie import network +from brownie import network, project from brownie.convert import Fixed, Wei from brownie.convert.utils import get_int_bounds @@ -18,8 +18,12 @@ class _DeferredStrategyRepr(DeferredStrategy): + def __init__(self, fn: Callable, repr_target: str) -> None: + super().__init__(fn) + self._repr_target = repr_target + def __repr__(self): - return "sampled_from(accounts)" + return f"sampled_from({self._repr_target})" def _exclude_filter(fn: Callable) -> Callable: @@ -73,7 +77,7 @@ def _decimal_strategy( @_exclude_filter def _address_strategy() -> SearchStrategy: - return _DeferredStrategyRepr(lambda: st.sampled_from(list(network.accounts))) + return _DeferredStrategyRepr(lambda: st.sampled_from(list(network.accounts)), "accounts") @_exclude_filter @@ -136,6 +140,17 @@ def _tuple_strategy(abi_type: TupleType) -> SearchStrategy: return st.tuples(*strategies) +def contract_strategy(contract_name: str) -> SearchStrategy: + def _contract_deferred(name): + for proj in project.get_loaded_projects(): + if name in proj.dict(): + return st.sampled_from(list(proj[name])) + + raise NameError(f"Contract '{name}' does not exist in any active projects") + + return _DeferredStrategyRepr(lambda: _contract_deferred(contract_name), contract_name) + + def strategy(type_str: str, **kwargs: Any) -> SearchStrategy: type_str = TYPE_STR_TRANSLATIONS.get(type_str, type_str) if type_str == "fixed168x10": From 8bc7331335117c470a28657424c13287a088dc6a Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Mon, 18 May 2020 18:58:41 +0400 Subject: [PATCH 2/3] docs: contract_strategy --- docs/tests-hypothesis-property.rst | 43 ++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/docs/tests-hypothesis-property.rst b/docs/tests-hypothesis-property.rst index 0b98d84b1..1b7d1ef2f 100644 --- a/docs/tests-hypothesis-property.rst +++ b/docs/tests-hypothesis-property.rst @@ -33,8 +33,19 @@ To begin writing property-based tests, import the following two methods: from brownie.test import given, strategy -* ``given`` is a decorator that converts a test function that accepts arguments into a randomized test. This is a thin wrapper around :func:`hypothesis.given `, the API is identical. -* ``strategy`` is a method for creating :ref:`test strategies` based on ABI types. +.. py:function:: brownie.test.given + + A decorator for turning a test function that accepts arguments into a randomized test. + + When using Brownie, this is the main entry point to property-based testing. This is a thin wrapper around :func:`hypothesis.given `, the API is identical. + + .. warning:: + + Be sure to import ``@given`` from Brownie and not directly from Hypothesis. Importing the function directly can cause issues with test isolation. + +.. py:function:: brownie.test.strategy + + A method for creating :ref:`test strategies` based on ABI types. A test using Hypothesis consists of two parts: A function that looks like a normal pytest test with some additional arguments, and a ``@given`` decorator that specifies how to those arguments are provided. @@ -294,6 +305,34 @@ This strategy does not accept any keyword arguments. >>> strategy('(uint16,bool)').example() (47628, False) +Contract Strategies +------------------- + +The ``contract_strategy`` function is used to draw from :func:`ProjectContract ` objects within a :func:`ContractContainer `. + + +.. py:function:: brownie.test.contract_strategy(contract_name) + + `Base strategy:` :func:`hypothesis.strategies.sampled_from ` + + A strategy to access :func:`ProjectContract ` objects. + + * ``contract_name``: The name of the contract, given as a string + + .. code-block:: python + + >>> ERC20 + [, ] + + >>> from brownie.test import contract_strategy + >>> contract_strategy('ERC20') + sampled_from(ERC20) + + >>> contract_strategy('ERC20').example() + + + + Other Strategies ---------------- From 35febd6ebd8488e982e51515db874bb6876aa07c Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Tue, 19 May 2020 02:05:09 +0400 Subject: [PATCH 3/3] test: contract_strategy --- .../test/strategies/test_contract_strategy.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/test/strategies/test_contract_strategy.py diff --git a/tests/test/strategies/test_contract_strategy.py b/tests/test/strategies/test_contract_strategy.py new file mode 100644 index 000000000..f2ee43606 --- /dev/null +++ b/tests/test/strategies/test_contract_strategy.py @@ -0,0 +1,26 @@ +#!/usr/bin/python3 + +import pytest +from hypothesis import given +from hypothesis.strategies._internal.deferred import DeferredStrategy + +from brownie.network.contract import ProjectContract +from brownie.test import contract_strategy + + +def test_strategy(): + assert isinstance(contract_strategy("ERC20"), DeferredStrategy) + + +def test_repr(): + assert repr(contract_strategy("Token")) == "sampled_from(Token)" + + +def test_does_not_exist(): + with pytest.raises(NameError): + contract_strategy("Foo").validate() + + +@given(contract=contract_strategy("BrownieTester")) +def test_given(tester, contract): + assert isinstance(contract, ProjectContract)