From 20928e41859cdb7dd8262ca7aeb91480a2e25764 Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Tue, 25 Jun 2019 16:57:36 +0200 Subject: [PATCH 1/5] Reworked test and deploy documentation - Split documentation for testing and deploying a contract - Completely revamped documentation for testing to use pytest and eth_tester. Complete with examples. - Updated documentation for deploying contracts - Added two new examples of very simple storage contracts that are used in the testing documentation - Added two new test files for said storage contracts - Updated the assert_tx_failed fixture in conftest with another optional argument to check for text in exceptions (so we can check for a specific ValidationError for example) --- docs/deploying-contracts.rst | 28 ++ docs/index.rst | 3 +- docs/testing-contracts.rst | 241 ++++++++++++++++++ docs/testing-deploying-contracts.rst | 77 ------ examples/storage/advanced_storage.vy | 18 ++ examples/storage/storage.vy | 9 + tests/conftest.py | 6 +- .../examples/storage/test_advanced_storage.py | 70 +++++ tests/examples/storage/test_storage.py | 29 +++ 9 files changed, 401 insertions(+), 80 deletions(-) create mode 100644 docs/deploying-contracts.rst create mode 100644 docs/testing-contracts.rst delete mode 100644 docs/testing-deploying-contracts.rst create mode 100644 examples/storage/advanced_storage.vy create mode 100644 examples/storage/storage.vy create mode 100644 tests/examples/storage/test_advanced_storage.py create mode 100644 tests/examples/storage/test_storage.py diff --git a/docs/deploying-contracts.rst b/docs/deploying-contracts.rst new file mode 100644 index 0000000000..4ff1519d1a --- /dev/null +++ b/docs/deploying-contracts.rst @@ -0,0 +1,28 @@ +.. index:: deploying;deploying; + +.. _deploying: + +******************** +Deploying a Contract +******************** + +Once you are ready to deploy your contract to a public test net or the main net, you have several options: + +* Take the bytecode generated by the vyper compiler and manually deploy it through mist or geth: + +.. code-block:: bash + + vyper yourFileName.vy + # returns bytecode + +* Take the byte code and ABI and depoly it with your current browser on `myetherwallet's `_ contract menu: + +.. code-block:: bash + + vyper -f abi yourFileName.vy + # returns ABI + +* Use the remote compiler provided by the `Remix IDE `_ to compile and deploy your contract on your net of choice. Remix also provides a JavaScript VM to test deploy your contract. + +.. note:: + While the vyper version of the Remix IDE compiler is updated on a regular basis it might be a bit behind the latest version found in the master branch of the repository. Make sure the byte code matches the output from your local compiler. \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index a3da07ce49..c92c0253af 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -97,7 +97,8 @@ Glossary installing-vyper.rst compiling-a-contract.rst - testing-deploying-contracts.rst + testing-contracts.rst + deploying-contracts.rst structure-of-a-contract.rst vyper-by-example.rst logging.rst diff --git a/docs/testing-contracts.rst b/docs/testing-contracts.rst new file mode 100644 index 0000000000..d0596056d6 --- /dev/null +++ b/docs/testing-contracts.rst @@ -0,0 +1,241 @@ +.. index:: testing;testing; + +.. _testing: + +****************** +Testing a Contract +****************** + +This documentation recommends the use of the `pytest `_ framework with the `ethereum-tester `_ package. +Prior to testing, the vyper specific contract conversion and the blockchain related fixtures need to be set up. These fixtures will be used in every test file and should therefore be defined in `conftest.py `_. + +================================= +Vyper Contract and Basic Fixtures +================================= + +.. code-block:: python + + import pytest + from eth_tester import EthereumTester + from vyper import compiler + + from web3 import Web3 + from web3.contract import ( + Contract, + mk_collision_prop, + ) + from web3.providers.eth_tester import EthereumTesterProvider + + from eth_utils.toolz import compose + + + class VyperMethod: + ALLOWED_MODIFIERS = {'call', 'estimateGas', 'transact', 'buildTransaction'} + + def __init__(self, function, normalizers=None): + self._function = function + self._function._return_data_normalizers = normalizers + + def __call__(self, *args, **kwargs): + return self.__prepared_function(*args, **kwargs) + + def __prepared_function(self, *args, **kwargs): + if not kwargs: + modifier, modifier_dict = 'call', {} + fn_abi = [ + x + for x + in self._function.contract_abi + if x.get('name') == self._function.function_identifier + ].pop() + # To make tests faster just supply some high gas value. + modifier_dict.update({'gas': fn_abi.get('gas', 0) + 50000}) + elif len(kwargs) == 1: + modifier, modifier_dict = kwargs.popitem() + if modifier not in self.ALLOWED_MODIFIERS: + raise TypeError( + "The only allowed keyword arguments are: %s" % self.ALLOWED_MODIFIERS) + else: + raise TypeError("Use up to one keyword argument, one of: %s" % self.ALLOWED_MODIFIERS) + + return getattr(self._function(*args), modifier)(modifier_dict) + + + class VyperContract: + + """ + An alternative Contract Factory which invokes all methods as `call()`, + unless you add a keyword argument. The keyword argument assigns the prep method. + + This call + + > contract.withdraw(amount, transact={'from': eth.accounts[1], 'gas': 100000, ...}) + + is equivalent to this call in the classic contract: + + > contract.functions.withdraw(amount).transact({'from': eth.accounts[1], 'gas': 100000, ...}) + """ + def __init__(self, classic_contract, method_class=VyperMethod): + + classic_contract._return_data_normalizers += CONCISE_NORMALIZERS + self._classic_contract = classic_contract + self.address = self._classic_contract.address + + protected_fn_names = [fn for fn in dir(self) if not fn.endswith('__')] + + for fn_name in self._classic_contract.functions: + + # Override namespace collisions + if fn_name in protected_fn_names: + _concise_method = mk_collision_prop(fn_name) + + else: + _classic_method = getattr( + self._classic_contract.functions, + fn_name) + + _concise_method = method_class( + _classic_method, + self._classic_contract._return_data_normalizers + ) + + setattr(self, fn_name, _concise_method) + + @classmethod + def factory(cls, *args, **kwargs): + return compose(cls, Contract.factory(*args, **kwargs)) + + + def _none_addr(datatype, data): + if datatype == 'address' and int(data, base=16) == 0: + return (datatype, None) + else: + return (datatype, data) + + + CONCISE_NORMALIZERS = (_none_addr, ) + + + @pytest.fixture + def tester(): + t = EthereumTester() + return t + + + def zero_gas_price_strategy(web3, transaction_params=None): + return 0 # zero gas price makes testing simpler. + + + @pytest.fixture + def w3(tester): + w3 = Web3(EthereumTesterProvider(tester)) + w3.eth.setGasPriceStrategy(zero_gas_price_strategy) + return w3 + + + def _get_contract(w3, source_code, *args, **kwargs): + out = compiler.compile_code( + source_code, + ['abi', 'bytecode'], + interface_codes=kwargs.pop('interface_codes', None), + ) + abi = out['abi'] + bytecode = out['bytecode'] + + value = kwargs.pop('value_in_eth', 0) * 10**18 # Handle deploying with an eth value. + + c = w3.eth.contract(abi=abi, bytecode=bytecode) + deploy_transaction = c.constructor(*args) + tx_info = { + 'from': w3.eth.accounts[0], + 'value': value, + 'gasPrice': 0, + } + tx_info.update(kwargs) + tx_hash = deploy_transaction.transact(tx_info) + address = w3.eth.getTransactionReceipt(tx_hash)['contractAddress'] + contract = w3.eth.contract( + address, + abi=abi, + bytecode=bytecode, + ContractFactoryClass=VyperContract, + ) + return contract + + + @pytest.fixture + def get_contract(w3): + def get_contract(source_code, *args, **kwargs): + return _get_contract(w3, source_code, *args, **kwargs) + return get_contract + +This is the minimum requirement to load a vyper contract and start testing. More fixtures and functions will be introduced later. +The rest of this chapter assumes, that you have this code set up in your ``conftest.py`` file. + +.. note:: + Since the testing is done in the pytest framework, you can make use of `pytest.ini, tox.ini and setup.cfg `_ and you can use most IDEs' pytest plugins. + +============================= +Load Contract and Basic Tests +============================= + +Assume the following simple contract ``storage.vy``. It has a single integer variable and a function to set that value. + +.. literalinclude:: ../examples/storage/storage.vy + :language: python + +We create a test file ``test_storage.py`` where we write our tests in pytest style. + +.. literalinclude:: ../tests/examples/storage/test_storage.py + :language: python + +First we create a fixture for the contract which will compile our contract and set up a Web3 contract object. +We then use this fixture for our test functions to interact with the contract. + +.. note:: + To run the tests, call ``pytest`` or ``python -m pytest`` from your project directory. + +============================== +Events and Failed Transactions +============================== + +To test events and failed transactions we expand our simple storage contract to include an event and two conditions for a failed transaction: ``advanced_storage.vy`` + +.. literalinclude:: ../examples/storage/advanced_storage.vy + :language: python + +Next, we add two new fixtures to ``conftest.py`` that will allow us to read the event logs and to check for failed transactions. + + +.. code-block:: python + + from eth_tester.exceptions import TransactionFailed + + @pytest.fixture + def get_logs(w3): + def get_logs(tx_hash, c, event_name): + tx_receipt = w3.eth.getTransactionReceipt(tx_hash) + logs = c._classic_contract.events[event_name]().processReceipt(tx_receipt) + return logs + return get_logs + + @pytest.fixture + def assert_tx_failed(tester): + def assert_tx_failed(function_to_test, exception=TransactionFailed, exc_text=None): + snapshot_id = tester.take_snapshot() + with pytest.raises(exception) as excinfo: + function_to_test() + tester.revert_to_snapshot(snapshot_id) + if exc_text: + assert exc_text in str(excinfo.value) + return assert_tx_failed + +The fixture to assert failed transactions defaults to check for a ``TransactionFailed`` exception, but can be used to check for different exceptions too, as shown below. +Also note that the chain gets reverted to the state before the failed transaction. + +Finally, we create a new file ``test_advanced_storage.py`` where we use the new fixtures to test failed transactions and events. + +.. literalinclude:: ../tests/examples/storage/test_advanced_storage.py + :language: python + + diff --git a/docs/testing-deploying-contracts.rst b/docs/testing-deploying-contracts.rst deleted file mode 100644 index 0da72c7769..0000000000 --- a/docs/testing-deploying-contracts.rst +++ /dev/null @@ -1,77 +0,0 @@ -.. index:: testing;deploying, testing; - -.. _testing_deploying: - -****************** -Testing a Contract -****************** - -The following example demonstrates how to compile and deploy your vyper contract. -It requires ``pyethereum>=2.0.0`` for the ``tester`` module - -.. code-block:: python - - from vyper import compiler - from ethereum.tools import tester - - # Get a new chain - chain = tester.Chain() - # Set the vyper compiler to run when the vyper language is requested - tester.languages['vyper'] = compiler.Compiler() - - with open('my_contract.vy' 'r') as f: - source_code = f.read() - # Compile and Deploy contract to provisioned testchain - # (e.g. run __init__ method) with given args (e.g. init_args) - # from msg.sender = t.k1 (private key of address 1 in test acconuts) - # and supply 1000 wei to the contract - init_args = ['arg1', 'arg2', 3] - contract = chain.contract(source_code, language="vyper", - init_args, sender=t.k1, value=1000) - - contract.myMethod() # Executes myMethod on the tester "chain" - chain.mine() # Mines the above transaction (and any before it) into a block - -Note: We are working on integration with `ethereum-tester `_, -so this example will change. - -=============================== -Testing Using vyper-run Command -=============================== - -To allow quickly testing contracts, Vyper provides a command line tool for instantly executing a function: -:: - - vyper-run yourFileName.vy "yourFunction();" -i some_init_param, another_init_param - -The vyper-run command is composed of 4 parts: - -- vyper-run - -- the name of the contract file you want to execute (for example: coolContract.vy) - -- a string (wrapped in double quotes) with the function you want to trigger, you can trigger multiple functions by adding a semicolon at the end of each function and then call the next function (for example: ``"myFunction1(100,4);myFunction2()"``) + - -- (Optional) the parameters for the ``__init__`` function of the contract (for example: given ``__init__(a: int128, b: int128)`` the syntax would be ``-i 8,27``). - -Putting it all together: -:: - - vyper-run myContract.vy "myFunction1();myFunction2()" -i 1,3 - -The vyper-run command will print out the returned value of the called functions as well as all the logged events emitted during the function's execution. - -******************** -Deploying a Contract -******************** - -You have several options to deploy a Vyper contract to the public testnets. - -One option is to take the bytecode generated by the vyper compiler and manually deploy it through mist or geth: - -.. code-block:: bash - - vyper yourFileName.vy - # returns bytecode - -Or deploy it with current browser on `myetherwallet `_ contract menu. diff --git a/examples/storage/advanced_storage.vy b/examples/storage/advanced_storage.vy new file mode 100644 index 0000000000..3a2bfeca28 --- /dev/null +++ b/examples/storage/advanced_storage.vy @@ -0,0 +1,18 @@ +DataChange: event({_setter: indexed(address), _value: int128}) + +storedData: public(int128) + +@public +def __init__(_x: int128): + self.storedData = _x + +@public +def set(_x: int128): + assert _x >= 0 # No negative values + assert self.storedData < 100 # Storage will lock when 100 or more is stored + self.storedData = _x + log.DataChange(msg.sender, _x) + +@public +def reset(): + self.storedData = 0 \ No newline at end of file diff --git a/examples/storage/storage.vy b/examples/storage/storage.vy new file mode 100644 index 0000000000..3897420a77 --- /dev/null +++ b/examples/storage/storage.vy @@ -0,0 +1,9 @@ +storedData: public(int128) + +@public +def __init__(_x: int128): + self.storedData = _x + +@public +def set(_x: int128): + self.storedData = _x \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 901edbace7..9c6d0f874c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -303,11 +303,13 @@ def get_contract_with_gas_estimation_for_constants( @pytest.fixture def assert_tx_failed(tester): - def assert_tx_failed(function_to_test, exception=TransactionFailed): + def assert_tx_failed(function_to_test, exception=TransactionFailed, exc_text=None): snapshot_id = tester.take_snapshot() - with pytest.raises(exception): + with pytest.raises(exception) as excinfo: function_to_test() tester.revert_to_snapshot(snapshot_id) + if exc_text: + assert exc_text in str(excinfo.value) return assert_tx_failed diff --git a/tests/examples/storage/test_advanced_storage.py b/tests/examples/storage/test_advanced_storage.py new file mode 100644 index 0000000000..93a8aa5cb4 --- /dev/null +++ b/tests/examples/storage/test_advanced_storage.py @@ -0,0 +1,70 @@ +import pytest +from web3.exceptions import ValidationError + +INITIAL_VALUE = 4 + + +@pytest.fixture +def adv_storage_contract(w3, get_contract): + with open('examples/storage/advanced_storage.vy') as f: + contract_code = f.read() + # Pass constructor variables directly to the contract + contract = get_contract(contract_code, INITIAL_VALUE) + return contract + + +def test_initial_state(adv_storage_contract): + # Check if the constructor of the contract is set up properly + assert adv_storage_contract.storedData() == INITIAL_VALUE + + +def test_failed_transactions(w3, adv_storage_contract, assert_tx_failed): + k1 = w3.eth.accounts[1] + + # Try to set the storage to a negative amount + assert_tx_failed(lambda: adv_storage_contract.set(-10, transact={"from": k1})) + + # Lock the contract by storing more than 100. Then try to change the value + adv_storage_contract.set(150, transact={"from": k1}) + assert_tx_failed(lambda: adv_storage_contract.set(10, transact={"from": k1})) + + # Reset the contract and try to change the value + adv_storage_contract.reset(transact={"from": k1}) + adv_storage_contract.set(10, transact={"from": k1}) + assert adv_storage_contract.storedData() == 10 + + # Assert a different exception (ValidationError for non matching argument type) + assert_tx_failed( + lambda: adv_storage_contract.set("foo", transact={"from": k1}), + ValidationError + ) + + # Assert a different exception that contains specific text + assert_tx_failed( + lambda: adv_storage_contract.set(1, 2, transact={"from": k1}), + ValidationError, + "invocation failed due to improper number of arguments", + ) + + +def test_events(w3, adv_storage_contract, get_logs): + k1, k2 = w3.eth.accounts[:2] + + tx1 = adv_storage_contract.set(10, transact={"from": k1}) + tx2 = adv_storage_contract.set(20, transact={"from": k2}) + tx3 = adv_storage_contract.reset(transact={"from": k1}) + + # Save DataChange logs from all three transactions + logs1 = get_logs(tx1, adv_storage_contract, "DataChange") + logs2 = get_logs(tx2, adv_storage_contract, "DataChange") + logs3 = get_logs(tx3, adv_storage_contract, "DataChange") + + # Check log contents + assert len(logs1) == 1 + assert logs1[0].args._value == 10 + + assert len(logs2) == 1 + assert logs2[0].args._setter == k2 + + assert not logs3 # tx3 does not generate a log + diff --git a/tests/examples/storage/test_storage.py b/tests/examples/storage/test_storage.py new file mode 100644 index 0000000000..6af90b4284 --- /dev/null +++ b/tests/examples/storage/test_storage.py @@ -0,0 +1,29 @@ +import pytest + +INITIAL_VALUE = 4 + + +@pytest.fixture +def storage_contract(w3, get_contract): + with open('examples/storage/storage.vy') as f: + contract_code = f.read() + # Pass constructor variables directly to the contract + contract = get_contract(contract_code, INITIAL_VALUE) + return contract + + +def test_initial_state(storage_contract): + # Check if the constructor of the contract is set up properly + assert storage_contract.storedData() == INITIAL_VALUE + + +def test_set(w3, storage_contract): + k0 = w3.eth.accounts[0] + + # Let k0 try to set the value to 10 + storage_contract.set(10, transact={"from": k0}) + assert storage_contract.storedData() == 10 # Directly access storedData + + # Let k0 try to set the value to -5 + storage_contract.set(-5, transact={"from": k0}) + assert storage_contract.storedData() == -5 From fde36ee2032747dec0b083f2aca74e85a5512df6 Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Tue, 25 Jun 2019 16:57:36 +0200 Subject: [PATCH 2/5] Reworked test and deploy documentation - Split documentation for testing and deploying a contract - Completely revamped documentation for testing to use pytest and eth_tester. Complete with examples. - Updated documentation for deploying contracts - Added two new examples of very simple storage contracts that are used in the testing documentation - Added two new test files for said storage contracts - Updated the assert_tx_failed fixture in conftest with another optional argument to check for text in exceptions (so we can check for a specific ValidationError for example) --- tests/examples/storage/test_advanced_storage.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/examples/storage/test_advanced_storage.py b/tests/examples/storage/test_advanced_storage.py index 93a8aa5cb4..793bd5dc8f 100644 --- a/tests/examples/storage/test_advanced_storage.py +++ b/tests/examples/storage/test_advanced_storage.py @@ -67,4 +67,3 @@ def test_events(w3, adv_storage_contract, get_logs): assert logs2[0].args._setter == k2 assert not logs3 # tx3 does not generate a log - From 9f52d7c9d55c4117a60dfc161eed75a39e6e111d Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Tue, 25 Jun 2019 16:57:36 +0200 Subject: [PATCH 3/5] Reworked test and deploy documentation - Split documentation for testing and deploying a contract - Completely revamped documentation for testing to use pytest and eth_tester. Complete with examples. - Updated documentation for deploying contracts - Added two new examples of very simple storage contracts that are used in the testing documentation - Added two new test files for said storage contracts - Updated the assert_tx_failed fixture in conftest with another optional argument to check for text in exceptions (so we can check for a specific ValidationError for example) --- tests/examples/storage/test_advanced_storage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/examples/storage/test_advanced_storage.py b/tests/examples/storage/test_advanced_storage.py index 793bd5dc8f..d0a5f62d63 100644 --- a/tests/examples/storage/test_advanced_storage.py +++ b/tests/examples/storage/test_advanced_storage.py @@ -1,4 +1,5 @@ import pytest + from web3.exceptions import ValidationError INITIAL_VALUE = 4 From 61fdc63dc575315fdc9ffffdf0f884d046f08f3c Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Tue, 25 Jun 2019 16:57:36 +0200 Subject: [PATCH 4/5] Reworked test and deploy documentation - Split documentation for testing and deploying a contract - Completely revamped documentation for testing to use pytest and eth_tester. Complete with examples. - Updated documentation for deploying contracts - Added two new examples of very simple storage contracts that are used in the testing documentation - Added two new test files for said storage contracts - Updated the assert_tx_failed fixture in conftest with another optional argument to check for text in exceptions (so we can check for a specific ValidationError for example) --- tests/examples/storage/test_advanced_storage.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/examples/storage/test_advanced_storage.py b/tests/examples/storage/test_advanced_storage.py index d0a5f62d63..6abdd23e00 100644 --- a/tests/examples/storage/test_advanced_storage.py +++ b/tests/examples/storage/test_advanced_storage.py @@ -1,6 +1,7 @@ import pytest - -from web3.exceptions import ValidationError +from web3.exceptions import ( + ValidationError, +) INITIAL_VALUE = 4 From 855a014009c6ee9b7285174e4e9d67e6fd4db494 Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Wed, 26 Jun 2019 18:26:05 +0200 Subject: [PATCH 5/5] Split conftest.py to base_conftest.py and conftest.py. Adapted Docs. test.base_conftest is not a plugin. base_conftest.py includes the base to start testing vyper contracts while conftest.py includes all the other fixtures and functions needed to test our code. --- docs/testing-contracts.rst | 209 +++++-------------------------------- tests/base_conftest.py | 173 ++++++++++++++++++++++++++++++ tests/conftest.py | 181 ++------------------------------ 3 files changed, 207 insertions(+), 356 deletions(-) create mode 100644 tests/base_conftest.py diff --git a/docs/testing-contracts.rst b/docs/testing-contracts.rst index d0596056d6..4a353753b5 100644 --- a/docs/testing-contracts.rst +++ b/docs/testing-contracts.rst @@ -6,174 +6,31 @@ Testing a Contract ****************** -This documentation recommends the use of the `pytest `_ framework with the `ethereum-tester `_ package. -Prior to testing, the vyper specific contract conversion and the blockchain related fixtures need to be set up. These fixtures will be used in every test file and should therefore be defined in `conftest.py `_. +This documentation recommends the use of the `pytest `_ framework with +the `ethereum-tester `_ package. +Prior to testing, the vyper specific contract conversion and the blockchain related fixtures need to be set up. +These fixtures will be used in every test file and should therefore be defined in +`conftest.py `_. + +.. note:: + Since the testing is done in the pytest framework, you can make use of + `pytest.ini, tox.ini and setup.cfg `_ and you can use most IDEs' + pytest plugins. ================================= Vyper Contract and Basic Fixtures ================================= -.. code-block:: python - - import pytest - from eth_tester import EthereumTester - from vyper import compiler - - from web3 import Web3 - from web3.contract import ( - Contract, - mk_collision_prop, - ) - from web3.providers.eth_tester import EthereumTesterProvider - - from eth_utils.toolz import compose - - - class VyperMethod: - ALLOWED_MODIFIERS = {'call', 'estimateGas', 'transact', 'buildTransaction'} - - def __init__(self, function, normalizers=None): - self._function = function - self._function._return_data_normalizers = normalizers - - def __call__(self, *args, **kwargs): - return self.__prepared_function(*args, **kwargs) - - def __prepared_function(self, *args, **kwargs): - if not kwargs: - modifier, modifier_dict = 'call', {} - fn_abi = [ - x - for x - in self._function.contract_abi - if x.get('name') == self._function.function_identifier - ].pop() - # To make tests faster just supply some high gas value. - modifier_dict.update({'gas': fn_abi.get('gas', 0) + 50000}) - elif len(kwargs) == 1: - modifier, modifier_dict = kwargs.popitem() - if modifier not in self.ALLOWED_MODIFIERS: - raise TypeError( - "The only allowed keyword arguments are: %s" % self.ALLOWED_MODIFIERS) - else: - raise TypeError("Use up to one keyword argument, one of: %s" % self.ALLOWED_MODIFIERS) - - return getattr(self._function(*args), modifier)(modifier_dict) - - - class VyperContract: - - """ - An alternative Contract Factory which invokes all methods as `call()`, - unless you add a keyword argument. The keyword argument assigns the prep method. - - This call - - > contract.withdraw(amount, transact={'from': eth.accounts[1], 'gas': 100000, ...}) - - is equivalent to this call in the classic contract: - - > contract.functions.withdraw(amount).transact({'from': eth.accounts[1], 'gas': 100000, ...}) - """ - def __init__(self, classic_contract, method_class=VyperMethod): - - classic_contract._return_data_normalizers += CONCISE_NORMALIZERS - self._classic_contract = classic_contract - self.address = self._classic_contract.address - - protected_fn_names = [fn for fn in dir(self) if not fn.endswith('__')] - - for fn_name in self._classic_contract.functions: - - # Override namespace collisions - if fn_name in protected_fn_names: - _concise_method = mk_collision_prop(fn_name) - - else: - _classic_method = getattr( - self._classic_contract.functions, - fn_name) +.. literalinclude:: ../tests/base_conftest.py + :language: python + :linenos: - _concise_method = method_class( - _classic_method, - self._classic_contract._return_data_normalizers - ) +This is the base requirement to load a vyper contract and start testing. The last two fixtures are optional and will be +discussed later. The rest of this chapter assumes, that you have this code set up in your ``conftest.py`` file. +Alternatively, you can import the fixtures to ``conftest.py`` or use +`pytest_plugins `_. - setattr(self, fn_name, _concise_method) - @classmethod - def factory(cls, *args, **kwargs): - return compose(cls, Contract.factory(*args, **kwargs)) - - - def _none_addr(datatype, data): - if datatype == 'address' and int(data, base=16) == 0: - return (datatype, None) - else: - return (datatype, data) - - - CONCISE_NORMALIZERS = (_none_addr, ) - - - @pytest.fixture - def tester(): - t = EthereumTester() - return t - - - def zero_gas_price_strategy(web3, transaction_params=None): - return 0 # zero gas price makes testing simpler. - - - @pytest.fixture - def w3(tester): - w3 = Web3(EthereumTesterProvider(tester)) - w3.eth.setGasPriceStrategy(zero_gas_price_strategy) - return w3 - - - def _get_contract(w3, source_code, *args, **kwargs): - out = compiler.compile_code( - source_code, - ['abi', 'bytecode'], - interface_codes=kwargs.pop('interface_codes', None), - ) - abi = out['abi'] - bytecode = out['bytecode'] - - value = kwargs.pop('value_in_eth', 0) * 10**18 # Handle deploying with an eth value. - - c = w3.eth.contract(abi=abi, bytecode=bytecode) - deploy_transaction = c.constructor(*args) - tx_info = { - 'from': w3.eth.accounts[0], - 'value': value, - 'gasPrice': 0, - } - tx_info.update(kwargs) - tx_hash = deploy_transaction.transact(tx_info) - address = w3.eth.getTransactionReceipt(tx_hash)['contractAddress'] - contract = w3.eth.contract( - address, - abi=abi, - bytecode=bytecode, - ContractFactoryClass=VyperContract, - ) - return contract - - - @pytest.fixture - def get_contract(w3): - def get_contract(source_code, *args, **kwargs): - return _get_contract(w3, source_code, *args, **kwargs) - return get_contract - -This is the minimum requirement to load a vyper contract and start testing. More fixtures and functions will be introduced later. -The rest of this chapter assumes, that you have this code set up in your ``conftest.py`` file. - -.. note:: - Since the testing is done in the pytest framework, you can make use of `pytest.ini, tox.ini and setup.cfg `_ and you can use most IDEs' pytest plugins. ============================= Load Contract and Basic Tests @@ -204,34 +61,22 @@ To test events and failed transactions we expand our simple storage contract to .. literalinclude:: ../examples/storage/advanced_storage.vy :language: python -Next, we add two new fixtures to ``conftest.py`` that will allow us to read the event logs and to check for failed transactions. +Next, we take a look at the two fixtures that will allow us to read the event logs and to check for failed transactions. +.. literalinclude:: ../tests/base_conftest.py + :language: python + :pyobject: assert_tx_failed -.. code-block:: python +The fixture to assert failed transactions defaults to check for a ``TransactionFailed`` exception, but can be used to check for different exceptions too, as shown below. +Also note that the chain gets reverted to the state before the failed transaction. - from eth_tester.exceptions import TransactionFailed - @pytest.fixture - def get_logs(w3): - def get_logs(tx_hash, c, event_name): - tx_receipt = w3.eth.getTransactionReceipt(tx_hash) - logs = c._classic_contract.events[event_name]().processReceipt(tx_receipt) - return logs - return get_logs +.. literalinclude:: ../tests/base_conftest.py + :language: python + :pyobject: get_logs - @pytest.fixture - def assert_tx_failed(tester): - def assert_tx_failed(function_to_test, exception=TransactionFailed, exc_text=None): - snapshot_id = tester.take_snapshot() - with pytest.raises(exception) as excinfo: - function_to_test() - tester.revert_to_snapshot(snapshot_id) - if exc_text: - assert exc_text in str(excinfo.value) - return assert_tx_failed +This fixture will return a tuple with all the logs for a certain event and transaction. The length of the tuple equals the number of events (of the specified type) logged and should be checked first. -The fixture to assert failed transactions defaults to check for a ``TransactionFailed`` exception, but can be used to check for different exceptions too, as shown below. -Also note that the chain gets reverted to the state before the failed transaction. Finally, we create a new file ``test_advanced_storage.py`` where we use the new fixtures to test failed transactions and events. diff --git a/tests/base_conftest.py b/tests/base_conftest.py new file mode 100644 index 0000000000..8f63df382f --- /dev/null +++ b/tests/base_conftest.py @@ -0,0 +1,173 @@ +from eth_tester import ( + EthereumTester, +) +from eth_tester.exceptions import ( + TransactionFailed, +) +from eth_utils.toolz import ( + compose, +) +import pytest +from web3 import Web3 +from web3.contract import ( + Contract, + mk_collision_prop, +) +from web3.providers.eth_tester import ( + EthereumTesterProvider, +) + +from vyper import ( + compiler, +) + + +class VyperMethod: + ALLOWED_MODIFIERS = {'call', 'estimateGas', 'transact', 'buildTransaction'} + + def __init__(self, function, normalizers=None): + self._function = function + self._function._return_data_normalizers = normalizers + + def __call__(self, *args, **kwargs): + return self.__prepared_function(*args, **kwargs) + + def __prepared_function(self, *args, **kwargs): + if not kwargs: + modifier, modifier_dict = 'call', {} + fn_abi = [ + x + for x + in self._function.contract_abi + if x.get('name') == self._function.function_identifier + ].pop() + # To make tests faster just supply some high gas value. + modifier_dict.update({'gas': fn_abi.get('gas', 0) + 50000}) + elif len(kwargs) == 1: + modifier, modifier_dict = kwargs.popitem() + if modifier not in self.ALLOWED_MODIFIERS: + raise TypeError( + "The only allowed keyword arguments are: %s" % self.ALLOWED_MODIFIERS) + else: + raise TypeError("Use up to one keyword argument, one of: %s" % self.ALLOWED_MODIFIERS) + return getattr(self._function(*args), modifier)(modifier_dict) + + +class VyperContract: + """ + An alternative Contract Factory which invokes all methods as `call()`, + unless you add a keyword argument. The keyword argument assigns the prep method. + This call + > contract.withdraw(amount, transact={'from': eth.accounts[1], 'gas': 100000, ...}) + is equivalent to this call in the classic contract: + > contract.functions.withdraw(amount).transact({'from': eth.accounts[1], 'gas': 100000, ...}) + """ + + def __init__(self, classic_contract, method_class=VyperMethod): + classic_contract._return_data_normalizers += CONCISE_NORMALIZERS + self._classic_contract = classic_contract + self.address = self._classic_contract.address + protected_fn_names = [fn for fn in dir(self) if not fn.endswith('__')] + for fn_name in self._classic_contract.functions: + # Override namespace collisions + if fn_name in protected_fn_names: + _concise_method = mk_collision_prop(fn_name) + else: + _classic_method = getattr( + self._classic_contract.functions, + fn_name) + _concise_method = method_class( + _classic_method, + self._classic_contract._return_data_normalizers + ) + setattr(self, fn_name, _concise_method) + + @classmethod + def factory(cls, *args, **kwargs): + return compose(cls, Contract.factory(*args, **kwargs)) + + +def _none_addr(datatype, data): + if datatype == 'address' and int(data, base=16) == 0: + return (datatype, None) + else: + return (datatype, data) + + +CONCISE_NORMALIZERS = (_none_addr,) + + +@pytest.fixture +def tester(): + t = EthereumTester() + return t + + +def zero_gas_price_strategy(web3, transaction_params=None): + return 0 # zero gas price makes testing simpler. + + +@pytest.fixture +def w3(tester): + w3 = Web3(EthereumTesterProvider(tester)) + w3.eth.setGasPriceStrategy(zero_gas_price_strategy) + return w3 + + +def _get_contract(w3, source_code, *args, **kwargs): + out = compiler.compile_code( + source_code, + ['abi', 'bytecode'], + interface_codes=kwargs.pop('interface_codes', None), + ) + abi = out['abi'] + bytecode = out['bytecode'] + value = kwargs.pop('value_in_eth', 0) * 10 ** 18 # Handle deploying with an eth value. + c = w3.eth.contract(abi=abi, bytecode=bytecode) + deploy_transaction = c.constructor(*args) + tx_info = { + 'from': w3.eth.accounts[0], + 'value': value, + 'gasPrice': 0, + } + tx_info.update(kwargs) + tx_hash = deploy_transaction.transact(tx_info) + address = w3.eth.getTransactionReceipt(tx_hash)['contractAddress'] + contract = w3.eth.contract( + address, + abi=abi, + bytecode=bytecode, + ContractFactoryClass=VyperContract, + ) + return contract + + +@pytest.fixture +def get_contract(w3): + def get_contract(source_code, *args, **kwargs): + return _get_contract(w3, source_code, *args, **kwargs) + + return get_contract + + +@pytest.fixture +def get_logs(w3): + def get_logs(tx_hash, c, event_name): + tx_receipt = w3.eth.getTransactionReceipt(tx_hash) + logs = c._classic_contract.events[event_name]().processReceipt(tx_receipt) + return logs + + return get_logs + + +@pytest.fixture +def assert_tx_failed(tester): + def assert_tx_failed(function_to_test, exception=TransactionFailed, exc_text=None): + snapshot_id = tester.take_snapshot() + with pytest.raises(exception) as excinfo: + function_to_test() + tester.revert_to_snapshot(snapshot_id) + if exc_text: + assert exc_text in str(excinfo.value) + + return assert_tx_failed diff --git a/tests/conftest.py b/tests/conftest.py index 9c6d0f874c..a53af68382 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,18 +6,8 @@ from eth_tester import ( EthereumTester, ) -from eth_tester.exceptions import ( - TransactionFailed, -) -from eth_utils.toolz import ( - compose, -) import pytest from web3 import Web3 -from web3.contract import ( - Contract, - mk_collision_prop, -) from web3.providers.eth_tester import ( EthereumTesterProvider, ) @@ -31,95 +21,15 @@ LLLnode, ) - -class VyperMethod: - ALLOWED_MODIFIERS = {'call', 'estimateGas', 'transact', 'buildTransaction'} - - def __init__(self, function, normalizers=None): - self._function = function - self._function._return_data_normalizers = normalizers - - def __call__(self, *args, **kwargs): - return self.__prepared_function(*args, **kwargs) - - def __prepared_function(self, *args, **kwargs): - if not kwargs: - modifier, modifier_dict = 'call', {} - fn_abi = [ - x - for x - in self._function.contract_abi - if x.get('name') == self._function.function_identifier - ].pop() - # To make tests faster just supply some high gas value. - modifier_dict.update({'gas': fn_abi.get('gas', 0) + 50000}) - elif len(kwargs) == 1: - modifier, modifier_dict = kwargs.popitem() - if modifier not in self.ALLOWED_MODIFIERS: - raise TypeError( - "The only allowed keyword arguments are: %s" % self.ALLOWED_MODIFIERS) - else: - raise TypeError("Use up to one keyword argument, one of: %s" % self.ALLOWED_MODIFIERS) - - return getattr(self._function(*args), modifier)(modifier_dict) - - -class VyperContract: - - """ - An alternative Contract Factory which invokes all methods as `call()`, - unless you add a keyword argument. The keyword argument assigns the prep method. - - This call - - > contract.withdraw(amount, transact={'from': eth.accounts[1], 'gas': 100000, ...}) - - is equivalent to this call in the classic contract: - - > contract.functions.withdraw(amount).transact({'from': eth.accounts[1], 'gas': 100000, ...}) - """ - def __init__(self, classic_contract, method_class=VyperMethod): - - classic_contract._return_data_normalizers += CONCISE_NORMALIZERS - self._classic_contract = classic_contract - self.address = self._classic_contract.address - - protected_fn_names = [fn for fn in dir(self) if not fn.endswith('__')] - - for fn_name in self._classic_contract.functions: - - # Override namespace collisions - if fn_name in protected_fn_names: - _concise_method = mk_collision_prop(fn_name) - - else: - _classic_method = getattr( - self._classic_contract.functions, - fn_name) - - _concise_method = method_class( - _classic_method, - self._classic_contract._return_data_normalizers - ) - - setattr(self, fn_name, _concise_method) - - @classmethod - def factory(cls, *args, **kwargs): - return compose(cls, Contract.factory(*args, **kwargs)) - - -def _none_addr(datatype, data): - if datatype == 'address' and int(data, base=16) == 0: - return (datatype, None) - else: - return (datatype, data) - - -CONCISE_NORMALIZERS = ( - _none_addr, +from .base_conftest import ( + VyperContract, + _get_contract, + zero_gas_price_strategy, ) +# Import the base_conftest fixtures +pytest_plugins = ['tests.base_conftest'] + ############ # PATCHING # ############ @@ -138,24 +48,6 @@ def set_evm_verbose_logging(): # from vdb import vdb # vdb.set_evm_opcode_debugger() - -@pytest.fixture -def tester(): - t = EthereumTester() - return t - - -def zero_gas_price_strategy(web3, transaction_params=None): - return 0 # zero gas price makes testing simpler. - - -@pytest.fixture -def w3(tester): - w3 = Web3(EthereumTesterProvider(tester)) - w3.eth.setGasPriceStrategy(zero_gas_price_strategy) - return w3 - - @pytest.fixture def keccak(): return Web3.keccak @@ -188,44 +80,6 @@ def lll_compiler(lll, *args, **kwargs): return lll_compiler -def _get_contract(w3, source_code, *args, **kwargs): - out = compiler.compile_code( - source_code, - ['abi', 'bytecode'], - interface_codes=kwargs.pop('interface_codes', None), - ) - abi = out['abi'] - bytecode = out['bytecode'] - contract = w3.eth.contract(abi=abi, bytecode=bytecode) - - value = kwargs.pop('value_in_eth', 0) * 10**18 # Handle deploying with an eth value. - - c = w3.eth.contract(abi=abi, bytecode=bytecode) - deploy_transaction = c.constructor(*args) - tx_info = { - 'from': w3.eth.accounts[0], - 'value': value, - 'gasPrice': 0, - } - tx_info.update(kwargs) - tx_hash = deploy_transaction.transact(tx_info) - address = w3.eth.getTransactionReceipt(tx_hash)['contractAddress'] - contract = w3.eth.contract( - address, - abi=abi, - bytecode=bytecode, - ContractFactoryClass=VyperContract, - ) - return contract - - -@pytest.fixture -def get_contract(w3): - def get_contract(source_code, *args, **kwargs): - return _get_contract(w3, source_code, *args, **kwargs) - return get_contract - - @pytest.fixture(scope='module') def get_contract_module(): tester = EthereumTester() @@ -301,18 +155,6 @@ def get_contract_with_gas_estimation_for_constants( return get_contract_with_gas_estimation_for_constants -@pytest.fixture -def assert_tx_failed(tester): - def assert_tx_failed(function_to_test, exception=TransactionFailed, exc_text=None): - snapshot_id = tester.take_snapshot() - with pytest.raises(exception) as excinfo: - function_to_test() - tester.revert_to_snapshot(snapshot_id) - if exc_text: - assert exc_text in str(excinfo.value) - return assert_tx_failed - - @pytest.fixture def assert_compile_failed(): def assert_compile_failed(function_to_test, exception=Exception): @@ -321,15 +163,6 @@ def assert_compile_failed(function_to_test, exception=Exception): return assert_compile_failed -@pytest.fixture -def get_logs(w3): - def get_logs(tx_hash, c, event_name): - tx_receipt = w3.eth.getTransactionReceipt(tx_hash) - logs = c._classic_contract.events[event_name]().processReceipt(tx_receipt) - return logs - return get_logs - - @pytest.fixture def search_for_sublist():