Search code examples
pythonethereumganachebrownie

Brownie testing for reverted transactions does not work with pytest.raises() or brownie.reverts()


Issue description: Brownie tests containing either

pytest.raises(exceptions.VirtualMachineError)

or

brownie.reverts()

producing an error, when the request by design fails (is reverted).

On GitHub: https://github.com/n4n0b1t3/brownie.reverts.example

Expected behavior: The test should either fail or succeed, depending on the outcome of the transaction.

Noticeable: When the test is set up in a way, that the transaction is not reverted, the test works correctly without producing an error.

What I have tried so far: I reduced the code to remove all unnecessary dependencies to contain only the problematic parts. After a complete new setup of a python virtual environment with only the necessary features (described below under setup), I ran the code again, to make sure the issue prevails and is not solved.

Source code of projectroot/sourcecode/contracts/ExampleContract.sol

// SPDX-License-Identifier: MIT

pragma solidity >=0.6.6 <0.9.0;

contract ExampleContract {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "only owner");
        _;
    }

    function withdraw() public payable onlyOwner {
        payable(msg.sender).transfer(address(this).balance);
    }
}

Exhibit A Source code of projectroot/sourcecode/test/test_reverts.py

import brownie
from brownie import ExampleContract, accounts, exceptions


def test_only_owner_can_withdraw():
    example_contract = ExampleContract.deploy({"from": accounts[0]})
    bad_account = accounts.add()
    with brownie.reverts():
        example_contract.withdraw({"from": bad_account})

Exhibit B Alternative making use of pytest.raises()

import brownie
import pytest
from brownie import ExampleContract, accounts, exceptions

def test_only_owner_can_withdraw():
    example_contract = ExampleContract.deploy({"from": accounts[0]})
    bad_account = accounts.add()
    with pytest.raises(exceptions.VirtualMachineError):
        example_contract.withdraw({"from": bad_account})

Exhibit C Alternative with not reverting transaction, which is correctly working:

import brownie
from brownie import ExampleContract, accounts, exceptions

def test_only_owner_can_withdraw():
    example_contract = ExampleContract.deploy({"from": accounts[0]})
    bad_account = accounts.add()
    with brownie.reverts():
        example_contract.withdraw({"from": accounts[0]})

With the result:

>raise AssertionError("Transaction did not revert")
E AssertionError: Transaction did not revert

Complete terminal output for exhibits A and B

(.venv) testcode>brownie test
INFORMATION: Es konnten keine Dateien mit dem angegebenen
Muster gefunden werden.
Brownie v1.16.4 - Python development framework for Ethereum

======================================================================================================= test session starts =======================================================================================================
platform win32 -- Python 3.10.2, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: C:\Users\Zarathustra\blockchainProjects\testing.revert\testcode
plugins: eth-brownie-1.16.4, hypothesis-6.21.6, forked-1.3.0, xdist-1.34.0, web3-5.23.1
collected 1 item

Launching 'ganache-cli.cmd --accounts 10 --hardfork istanbul --gasLimit 12000000 --mnemonic brownie --port 8545'...

tests\test_reverts.py F                                                                                                                                                                                                      [100%]

============================================================================================================ FAILURES =============================================================================================================
__________________________________________________________________________________________________ test_only_owner_can_withdraw ___________________________________________________________________________________________________

    def test_only_owner_can_withdraw():
        fund_me = ExampleContract.deploy({"from": accounts[0]})
        print(f"FundMe deployed to {fund_me.address}")
        good = accounts[0]
        bad = accounts.add()
        # with pytest.raises(exceptions.VirtualMachineError):
>       with brownie.reverts():

tests\test_reverts.py:12:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
tests\test_reverts.py:13: in test_only_owner_can_withdraw
    fund_me.withdraw({"from": bad})
..\.venv\lib\site-packages\brownie\network\contract.py:1693: in __call__
    return self.transact(*args)
..\.venv\lib\site-packages\brownie\network\contract.py:1566: in transact
    return tx["from"].transfer(
..\.venv\lib\site-packages\brownie\network\account.py:680: in transfer
    receipt._raise_if_reverted(exc)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <Transaction '0x1f310675db8ad41f0eb4cd9338f35f2800174168b18c019645b5b1d82ade2826'>, exc = None

    def _raise_if_reverted(self, exc: Any) -> None:
        if self.status or CONFIG.mode == "console":
            return
        if not web3.supports_traces:
            # if traces are not available, do not attempt to determine the revert reason
            raise exc or ValueError("Execution reverted")

        if self._dev_revert_msg is None:
            # no revert message and unable to check dev string - have to get trace
            self._expand_trace()
        if self.contract_address:
            source = ""
        elif CONFIG.argv["revert"]:
            source = self._traceback_string()
        else:
            source = self._error_string(1)
            contract = state._find_contract(self.receiver)
            if contract:
                marker = "//" if contract._build["language"] == "Solidity" else "#"
                line = self._traceback_string().split("\n")[-1]
                if marker + " dev: " in line:
                    self._dev_revert_msg = line[line.index(marker) + len(marker) : -5].strip()

>       raise exc._with_attr(
            source=source, revert_msg=self._revert_msg, dev_revert_msg=self._dev_revert_msg
        )
E       AttributeError: 'NoneType' object has no attribute '_with_attr'

..\.venv\lib\site-packages\brownie\network\transaction.py:420: AttributeError
------------------------------------------------------------------------------------------------------ Captured stdout call ------------------------------------------------------------------------------------------------------- 
FundMe deployed to 0x3194cBDC3dbcd3E11a07892e7bA5c3394048Cc87
mnemonic: 'abandon wisdom slot exclude buyer raccoon desert grid inmate flag state castle'
===================================================================================================== short test summary info ===================================================================================================== 
FAILED tests/test_reverts.py::test_only_owner_can_withdraw - AttributeError: 'NoneType' object has no attribute '_with_attr'
======================================================================================================== 1 failed in 6.40s ======================================================================================================== 
Terminating local RPC client...
(.venv) testcode>

My setting: The root directory contains the .venv virtual python environment. Under venv Cython and Brownie are installed. In VSC I am using the latest solidity compiler. In order to install ganache I opened a powershell with admin rights and activated the virtual environment, installed nodeenv, and connected it with venv. After this step I installed ganache and tested it. Everything is working correctly. Complete projects are tested and work correctly in this setup, besides tests with brownie.reverts().

Here in short the command line overview:

projectroot>.\.venv\Scripts\Activate.ps1
(.venv) projectroot>pip install nodeenv
(.venv) projectroot>nodeenv -p
(.venv) projectroot>npm install -g ganache
(.venv) projectroot>ganache --version
(.venv) projectroot>mkdir sourcecode
(.venv) projectroot>cd sourcecode
(.venv) sourcecode>brownie init
(.venv) sourcecode>brownie --version

Solution

  • This is fixed in Brownie v1.18.1. However, you will need to install Python 3.9.10 in order to get the latest brownie. For this to work in a virtual environment you can't use venv. Here is a process that works for me:

    • install virtualenv on your standard Python version
    • download python 3.9.10 and install it without "add to path" into a dedicated directory e.g. $home/pythonversions
    • in your project directory create a virtual environment like so

    python -m virtualenv -p="<path to the python executable >" <virtual_environment_directory>

    • start your virtual environment e.g. home>..venv\Scripts\activate.ps1

    • test if your python version is the desired one with python --version

    • Now install Cython to avoid another error.

    • Install nodeenv in order to install ganage

    • Activate with nodeenv -p (for this step you will need PowerShell with admin rights)

    • Install ganache with npm

    • Install eth-brownie with pip check if you got the latest version with

    brownie --version