2020-05-19 21:54:56 +00:00
|
|
|
from enum import Enum
|
|
|
|
|
2020-05-21 22:39:01 +00:00
|
|
|
from constant_sorrow.constants import (CONTRACT_ATTRIBUTE, CONTRACT_CALL, TRANSACTION)
|
2020-05-04 07:52:03 +00:00
|
|
|
from hexbytes import HexBytes
|
2020-05-19 21:54:56 +00:00
|
|
|
from typing import Callable, Generator, Iterable, List, Type, Union
|
2020-05-21 22:39:01 +00:00
|
|
|
from unittest.mock import Mock
|
2020-05-19 19:30:41 +00:00
|
|
|
|
|
|
|
from nucypher.blockchain.eth import agents
|
2020-05-21 23:01:26 +00:00
|
|
|
from nucypher.blockchain.eth.agents import Agent, ContractAgency, EthereumContractAgent
|
2020-05-03 01:29:53 +00:00
|
|
|
from nucypher.blockchain.eth.constants import NULL_ADDRESS
|
2020-05-02 23:42:14 +00:00
|
|
|
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
|
2020-05-13 18:24:36 +00:00
|
|
|
from tests.constants import MOCK_PROVIDER_URI
|
2020-05-02 23:42:14 +00:00
|
|
|
|
2020-05-04 07:52:03 +00:00
|
|
|
MOCK_TESTERCHAIN = BlockchainInterfaceFactory.get_or_create_interface(provider_uri=MOCK_PROVIDER_URI)
|
|
|
|
CURRENT_BLOCK = MOCK_TESTERCHAIN.w3.eth.getBlock(block_identifier='latest')
|
2020-05-02 23:42:14 +00:00
|
|
|
|
|
|
|
|
2020-05-19 21:54:56 +00:00
|
|
|
class MockContractAgent:
|
2020-05-03 01:29:53 +00:00
|
|
|
|
2020-05-19 21:54:56 +00:00
|
|
|
FAKE_RECEIPT = {'transactionHash': HexBytes(b'FAKE29890FAKE8349804'),
|
|
|
|
'gasUsed': 1,
|
|
|
|
'blockNumber': CURRENT_BLOCK.number,
|
|
|
|
'blockHash': HexBytes(b'FAKE43434343FAKE43443434')}
|
2020-05-03 01:29:53 +00:00
|
|
|
|
2020-05-19 21:54:56 +00:00
|
|
|
FAKE_CALL_RESULT = 1
|
2020-05-03 01:29:53 +00:00
|
|
|
|
2020-05-19 21:54:56 +00:00
|
|
|
# Internal
|
|
|
|
__COLLECTION_MARKER = "contract_api" # decorator attribute
|
|
|
|
__DEFAULTS = {
|
2020-05-21 22:39:01 +00:00
|
|
|
CONTRACT_CALL: FAKE_CALL_RESULT,
|
|
|
|
CONTRACT_ATTRIBUTE: FAKE_CALL_RESULT,
|
|
|
|
TRANSACTION: FAKE_RECEIPT,
|
2020-05-19 21:54:56 +00:00
|
|
|
}
|
2020-05-04 07:52:03 +00:00
|
|
|
|
2020-05-19 21:54:56 +00:00
|
|
|
_MOCK_METHODS = list()
|
|
|
|
_REAL_METHODS = list()
|
2020-05-02 23:42:14 +00:00
|
|
|
|
2020-05-19 21:54:56 +00:00
|
|
|
# Mock Nucypher Contract API
|
|
|
|
contract = Mock()
|
|
|
|
contract_address = NULL_ADDRESS
|
2020-05-19 06:25:11 +00:00
|
|
|
|
2020-05-19 21:54:56 +00:00
|
|
|
# Mock Blockchain Interfaces
|
2020-05-03 01:29:53 +00:00
|
|
|
registry = Mock()
|
2020-05-04 07:52:03 +00:00
|
|
|
blockchain = MOCK_TESTERCHAIN
|
2020-05-03 01:29:53 +00:00
|
|
|
|
2020-05-19 21:54:56 +00:00
|
|
|
def __init__(self, agent_class: Type[EthereumContractAgent]):
|
|
|
|
"""Bind mock agent attributes to the *subclass* with default values"""
|
|
|
|
self.agent_class = agent_class
|
|
|
|
self.__setup_mock(agent_class=agent_class)
|
2020-05-03 01:29:53 +00:00
|
|
|
|
2020-05-20 18:14:15 +00:00
|
|
|
def __repr__(self) -> str:
|
|
|
|
r = f'Mock{self.agent_class.__name__}(id={id(self)})'
|
|
|
|
return r
|
|
|
|
|
2020-05-19 21:54:56 +00:00
|
|
|
@classmethod
|
2020-05-21 23:01:26 +00:00
|
|
|
def __setup_mock(cls, agent_class: Type[Agent]) -> None:
|
2020-05-14 23:26:35 +00:00
|
|
|
|
2020-05-21 23:01:26 +00:00
|
|
|
api_methods: Iterable[Callable] = list(cls.__collect_contract_api(agent_class=agent_class))
|
2020-05-20 18:14:15 +00:00
|
|
|
mock_methods, mock_properties = list(), dict()
|
|
|
|
|
|
|
|
for agent_interface in api_methods:
|
|
|
|
|
|
|
|
# Handle
|
|
|
|
try:
|
2020-05-21 23:01:26 +00:00
|
|
|
# TODO: #2022: This might be a method also decorated @property
|
|
|
|
# Get the inner function of the property
|
|
|
|
real_method: Callable = agent_interface.fget # Handle properties
|
2020-05-20 18:14:15 +00:00
|
|
|
except AttributeError:
|
|
|
|
real_method = agent_interface
|
|
|
|
|
|
|
|
# Get
|
|
|
|
interface = getattr(real_method, cls.__COLLECTION_MARKER)
|
2020-05-19 21:54:56 +00:00
|
|
|
default_return = cls.__DEFAULTS.get(interface)
|
2020-05-19 19:30:41 +00:00
|
|
|
|
2020-05-21 23:01:26 +00:00
|
|
|
# TODO: #2022 Special handling of PropertyMocks?
|
2020-05-20 18:14:15 +00:00
|
|
|
# # Setup
|
2020-05-21 22:39:01 +00:00
|
|
|
# if interface == CONTRACT_ATTRIBUTE:
|
2020-05-20 18:14:15 +00:00
|
|
|
# mock = PropertyMock()
|
|
|
|
# mock_properties[real_method.__name__] = mock
|
|
|
|
# else:
|
2020-05-19 21:54:56 +00:00
|
|
|
mock = Mock(return_value=default_return)
|
2020-05-20 18:14:15 +00:00
|
|
|
|
|
|
|
# Mark
|
2020-05-19 21:54:56 +00:00
|
|
|
setattr(mock, cls.__COLLECTION_MARKER, interface)
|
|
|
|
mock_methods.append(mock)
|
2020-05-03 01:29:53 +00:00
|
|
|
|
2020-05-20 18:14:15 +00:00
|
|
|
# Bind
|
|
|
|
setattr(cls, real_method.__name__, mock)
|
2020-05-19 21:54:56 +00:00
|
|
|
|
|
|
|
cls._MOCK_METHODS = mock_methods
|
2020-05-20 18:14:15 +00:00
|
|
|
cls._REAL_METHODS = api_methods
|
2020-05-04 07:52:03 +00:00
|
|
|
|
2020-05-14 23:26:35 +00:00
|
|
|
@classmethod
|
2020-05-19 21:54:56 +00:00
|
|
|
def __get_interface_calls(cls, interface: Enum) -> List[Callable]:
|
|
|
|
predicate = lambda method: bool(method.contract_api == interface)
|
|
|
|
interface_calls = list(filter(predicate, cls._MOCK_METHODS))
|
|
|
|
return interface_calls
|
|
|
|
|
|
|
|
@classmethod
|
2020-05-21 23:01:26 +00:00
|
|
|
def __is_contract_method(cls, agent_class: Type[Agent], method_name: str) -> bool:
|
2020-05-20 18:14:15 +00:00
|
|
|
method_or_property = getattr(agent_class, method_name)
|
|
|
|
try:
|
|
|
|
real_method: Callable = method_or_property.fget # Property (getter)
|
|
|
|
except AttributeError:
|
|
|
|
real_method: Callable = method_or_property # Method
|
|
|
|
contract_api: bool = hasattr(real_method, cls.__COLLECTION_MARKER)
|
|
|
|
return contract_api
|
2020-05-04 15:37:54 +00:00
|
|
|
|
2020-05-19 21:54:56 +00:00
|
|
|
@classmethod
|
2020-05-21 23:01:26 +00:00
|
|
|
def __collect_contract_api(cls, agent_class: Type[Agent]) -> Generator[Callable, None, None]:
|
2020-05-19 21:54:56 +00:00
|
|
|
agent_attrs = dir(agent_class)
|
|
|
|
predicate = cls.__is_contract_method
|
|
|
|
methods = (getattr(agent_class, name) for name in agent_attrs if predicate(agent_class, name))
|
2020-05-19 19:30:41 +00:00
|
|
|
return methods
|
2020-05-04 15:37:54 +00:00
|
|
|
|
2020-05-19 21:54:56 +00:00
|
|
|
#
|
|
|
|
# Test Utilities
|
|
|
|
#
|
|
|
|
|
|
|
|
@property
|
|
|
|
def all_transactions(self) -> List[Callable]:
|
2020-05-21 22:39:01 +00:00
|
|
|
interface = TRANSACTION
|
2020-05-19 21:54:56 +00:00
|
|
|
transaction_functions = self.__get_interface_calls(interface=interface)
|
|
|
|
return transaction_functions
|
|
|
|
|
|
|
|
@property
|
|
|
|
def contract_calls(self) -> List[Callable]:
|
2020-05-21 22:39:01 +00:00
|
|
|
interface = CONTRACT_CALL
|
2020-05-19 21:54:56 +00:00
|
|
|
transaction_functions = self.__get_interface_calls(interface=interface)
|
|
|
|
return transaction_functions
|
|
|
|
|
|
|
|
def get_unexpected_transactions(self, allowed: Union[Iterable[Callable], None]) -> List[Callable]:
|
|
|
|
if allowed:
|
|
|
|
predicate = lambda tx: tx not in allowed and tx.called
|
|
|
|
else:
|
|
|
|
predicate = lambda tx: tx.called
|
|
|
|
unexpected_transactions = list(filter(predicate, self.all_transactions))
|
|
|
|
return unexpected_transactions
|
|
|
|
|
2020-05-21 17:20:25 +00:00
|
|
|
def assert_only_transactions(self, allowed: Iterable[Callable]) -> None:
|
2020-05-19 21:54:56 +00:00
|
|
|
unexpected_transactions = self.get_unexpected_transactions(allowed=allowed)
|
|
|
|
assert not bool(unexpected_transactions)
|
|
|
|
|
|
|
|
def assert_no_transactions(self) -> None:
|
|
|
|
unexpected_transactions = self.get_unexpected_transactions(allowed=None)
|
|
|
|
assert not bool(unexpected_transactions)
|
|
|
|
|
2020-05-20 00:11:07 +00:00
|
|
|
def reset(self, clear_side_effects: bool = True) -> None:
|
2020-05-19 21:54:56 +00:00
|
|
|
for mock in self._MOCK_METHODS:
|
|
|
|
mock.reset_mock()
|
2020-05-20 00:11:07 +00:00
|
|
|
if clear_side_effects:
|
|
|
|
mock.side_effect = None
|
2020-05-04 08:55:59 +00:00
|
|
|
|
2020-05-20 00:11:07 +00:00
|
|
|
|
2020-05-19 19:30:41 +00:00
|
|
|
class MockContractAgency(ContractAgency):
|
2020-05-04 07:52:03 +00:00
|
|
|
|
2020-05-19 21:54:56 +00:00
|
|
|
__agents = dict()
|
|
|
|
|
2020-05-04 07:52:03 +00:00
|
|
|
@classmethod
|
2020-05-21 23:01:26 +00:00
|
|
|
def get_agent(cls, agent_class: Type[Agent], *args, **kwargs) -> Type[MockContractAgent]:
|
2020-05-19 21:54:56 +00:00
|
|
|
try:
|
|
|
|
mock_agent = cls.__agents[agent_class]
|
|
|
|
except KeyError:
|
|
|
|
mock_agent = MockContractAgent(agent_class=agent_class)
|
|
|
|
cls.__agents[agent_class] = mock_agent
|
2020-05-19 19:30:41 +00:00
|
|
|
return mock_agent
|
2020-05-04 07:52:03 +00:00
|
|
|
|
|
|
|
@classmethod
|
2020-05-21 23:01:26 +00:00
|
|
|
def get_agent_by_contract_name(cls, contract_name: str, *args, **kwargs) -> Type[MockContractAgent]:
|
2020-05-19 19:30:41 +00:00
|
|
|
agent_name = super()._contract_name_to_agent_name(name=contract_name)
|
|
|
|
agent_class = getattr(agents, agent_name)
|
|
|
|
mock_agent = cls.get_agent(agent_class=agent_class)
|
|
|
|
return mock_agent
|
2020-05-19 22:25:34 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def reset(cls) -> None:
|
|
|
|
for agent in cls.__agents.values():
|
|
|
|
agent.reset()
|