eth-brownie / brownie

A Python-based development and testing framework for smart contracts targeting the Ethereum Virtual Machine.
https://eth-brownie.readthedocs.io
MIT License
2.66k stars 551 forks source link

IndexError raised when passing an element of brownie.accounts as the exclude argument to brownie.test.strategy #918

Open skellet0r opened 3 years ago

skellet0r commented 3 years ago

Environment information

What was wrong?

Command: brownie test Code that caused failure:

from brownie import accounts
from brownie.test import given, strategy

@given(
    amount=strategy("uint256", max_value="1000 ether"),
    to=strategy("address", exclude=accounts[0]),
)
def test_transfer_success_returns_true(token, accounts, amount, to):
    assert token.transfer(to, amount, {"from": accounts[0]}) == True

Error output:

$ brownie test tests/test_transfer.py
Brownie v1.12.3 - Python development framework for Ethereum

=============================================================================================== test session starts ===============================================================================================
platform linux -- Python 3.8.3, pytest-6.0.1, py-1.10.0, pluggy-0.13.1
rootdir: /home/skelletor/projects/CloudToken
plugins: eth-brownie-1.12.3, hypothesis-5.41.3, forked-1.3.0, web3-5.11.1, xdist-1.34.0
collected 0 items / 1 error                                                                                                                                                                                       

===================================================================================================== ERRORS ======================================================================================================
_____________________________________________________________________________________ ERROR collecting tests/test_transfer.py _____________________________________________________________________________________
tests/test_transfer.py:8: in <module>
    to=strategy("address", exclude=accounts[0]),
E   IndexError: list index out of range
============================================================================================= short test summary info =============================================================================================
FAILED tests/test_transfer.py - IndexError: list index out of range
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
================================================================================================ 1 error in 0.15s =================================================================================================

Extra Information

sabotagebeats commented 3 years ago

able to recreate on v1.13.1

sabotagebeats commented 3 years ago

original code from the blog does have an issue confusing the receiver and to variables, but this has been resolved in the code posted above, and is not causing the specified error.

sabotagebeats commented 3 years ago

assert len(accounts) > 0 after the import statements shows accounts is empty which is why it fails IndexError

_______________________ ERROR collecting tests/test_transfer_hypothesis2.py ________________________
tests/test_transfer_hypothesis2.py:5: in <module>
    assert len(accounts) >0
E   assert 0 > 0
E    +  where 0 = len(<brownie.network.account.Accounts object at 0x7f0d9ac16c70>)
sabotagebeats commented 3 years ago

Since the following test passes for me, I can conclude that accounts is empty when being fed to strategy within the @given decorator, but is populated when being fed to the function. I can also conclude that other than this, the test is working to exclude the listed value.

#!/usr/bin/python3
from brownie import accounts
from brownie.test import given, strategy

@given(
  receiver=strategy('address', exclude='0x66aB6D9362d4F35596279692F0251Db635165871'), # this is accounts[0]
  amount=strategy('uint256', max_value=10**18),
)
def test_transfer_adjusts_receiver_balance(accounts, token, receiver, amount):
    balance = token.balanceOf(receiver)
    token.transfer(receiver, amount, {'from': accounts[0]})
    assert receiver != accounts[0]
    assert token.balanceOf(receiver) == balance + amount
sabotagebeats commented 3 years ago

I was able to get this code successfully running by modifying as follows:

#!/usr/bin/python3
from brownie import accounts
from brownie.test import given, strategy

def test_transfer_adjusts_receiver_balance(accounts, token):

    @given(
    receiver=strategy('address', exclude=accounts[0]),
    amount=strategy('uint256', max_value=10**18),
    )
    def run(accounts, token, receiver, amount):
        balance = token.balanceOf(receiver)
        token.transfer(receiver, amount, {'from': accounts[0]})
        assert receiver != accounts[0]
        assert token.balanceOf(receiver) == balance + amount

    run(accounts, token)

The accounts object is not loaded up until pytest initializes brownie, which doesn't happen until the function is ran. So I wrapped it in an additional function which then feeds the accounts to the hypothesis decorator. If I modify my assertion operators I can see the code is now working.

sabotagebeats commented 3 years ago
#!/usr/bin/python3
from brownie import accounts
from brownie.test import given, strategy

from brownie import network
network.connect('development')

@given(
receiver=strategy('address', exclude=accounts[0]),
amount=strategy('uint256', max_value=10**18),
)
def test_transfer_adjusts_receiver_balance(accounts, token, receiver, amount):
    balance = token.balanceOf(receiver)
    token.transfer(receiver, amount, {'from': accounts[0]})
    assert receiver != accounts[0]
    assert token.balanceOf(receiver) == balance + amount

this code works, now need to see if I can inject network.connect('development') as a lambda function, possibly in given, or possibly as a fixture?

sabotagebeats commented 3 years ago

I'm able to get it to work by placing brownie.network.connect('development') within the tests\conftest.py

sabotagebeats commented 3 years ago

I am still getting the error when I try to move the line to anywhere within brownie\test\strategies.py or brownie\test\__init__.py both within and outside of the address strategy function.

iamdefinitelyahuman commented 3 years ago

Took a look at this.. As you said, the problem is that test collection happens prior to connecting to the network, so accounts is empty.

Connecting to the network is handled in pytest_collection_finish, in brownie/test/runner.py, line 258. This is a deliberate design choice for 2 reasons:

  1. If no tests were collected, or there is an error during collection, we don't have to wait for ganache-cli to launch and be killed.
  2. It allows for individual test suites to implement their own hook point and connect to a different network (example)

There is no easy solution I can see here unfortunately, as the call to strategy happens immediatly upon importing the test module. A hacky solution might be to allow Accounts to return a sort-of proxy "promise" object that represents the eventual Account that will exist at index 0 once the network has connect. The first attempt to inspect this object would then mutate it into the actual account object, or raise if the index is out of bounds.

sabotagebeats commented 3 years ago

I was hoping to find a solution which would wrap this for the user so that they don't have to worry about it, however it seems that it's functioning as designed and so this won't be feasible? If this is the case, alternatively could we just modify the documentation for the address strategy in order to notate that it requires loading the accounts object first?

iluxonchik commented 1 year ago

I experienced the same problem in my test suite and found a clean workaround by altering what I am parameterizing. Instead of passing the account in the strategy, I passed the index of the account, after which I obtain the actual account through its index.