2020-05-04 08:55:59 +00:00
|
|
|
from collections import defaultdict
|
2020-05-04 15:37:54 +00:00
|
|
|
from typing import Tuple
|
|
|
|
|
2020-05-04 08:55:59 +00:00
|
|
|
from functools import partial
|
|
|
|
|
2020-05-04 07:52:03 +00:00
|
|
|
from hexbytes import HexBytes
|
2020-05-02 23:42:14 +00:00
|
|
|
from unittest.mock import Mock
|
|
|
|
|
2020-05-04 07:52:03 +00:00
|
|
|
from nucypher.blockchain.economics import EconomicsFactory
|
2020-05-05 22:22:24 +00:00
|
|
|
from nucypher.blockchain.eth.agents import WorkLockAgent, StakingEscrowAgent, NucypherTokenAgent, PolicyManagerAgent
|
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
|
|
|
|
from nucypher.utilities.sandbox.constants import MOCK_PROVIDER_URI
|
|
|
|
|
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-03 00:01:14 +00:00
|
|
|
#
|
2020-05-02 23:42:14 +00:00
|
|
|
# Fixtures
|
2020-05-03 00:01:14 +00:00
|
|
|
#
|
|
|
|
|
2020-05-02 23:42:14 +00:00
|
|
|
FAKE_RECEIPT = {'transactionHash': HexBytes(b'FAKE29890FAKE8349804'),
|
|
|
|
'gasUsed': 1,
|
2020-05-04 07:52:03 +00:00
|
|
|
'blockNumber': CURRENT_BLOCK.number,
|
2020-05-02 23:42:14 +00:00
|
|
|
'blockHash': HexBytes(b'FAKE43434343FAKE43443434')}
|
|
|
|
|
|
|
|
|
2020-05-03 01:29:53 +00:00
|
|
|
def fake_transaction(*_a, **_kw) -> dict:
|
2020-05-03 00:01:14 +00:00
|
|
|
return FAKE_RECEIPT
|
|
|
|
|
|
|
|
|
2020-05-03 01:29:53 +00:00
|
|
|
def fake_call(*_a, **_kw) -> 1:
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
# Agents
|
|
|
|
#
|
|
|
|
|
2020-05-04 07:52:03 +00:00
|
|
|
|
2020-05-02 23:42:14 +00:00
|
|
|
class MockContractAgent:
|
|
|
|
|
2020-05-04 08:55:59 +00:00
|
|
|
# Internal
|
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-03 00:01:14 +00:00
|
|
|
contract = Mock()
|
2020-05-03 01:29:53 +00:00
|
|
|
contract_address = NULL_ADDRESS
|
|
|
|
|
2020-05-04 08:55:59 +00:00
|
|
|
# API
|
2020-05-04 07:52:03 +00:00
|
|
|
ATTRS = dict()
|
|
|
|
CALLS = tuple()
|
|
|
|
TRANSACTIONS = tuple()
|
2020-05-03 01:29:53 +00:00
|
|
|
|
2020-05-04 08:55:59 +00:00
|
|
|
# Spy
|
|
|
|
_SPY_TRANSACTIONS = defaultdict(list)
|
|
|
|
_SPY_CALLS = defaultdict(list)
|
|
|
|
|
2020-05-03 01:29:53 +00:00
|
|
|
def __init__(self):
|
2020-05-04 07:52:03 +00:00
|
|
|
|
|
|
|
# Bind mock agent attributes to the *subclass*
|
|
|
|
for agent_method, mock_value in self.ATTRS.items():
|
|
|
|
setattr(self.__class__, agent_method, mock_value)
|
2020-05-04 21:15:37 +00:00
|
|
|
self.__setup_mock()
|
2020-05-02 23:42:14 +00:00
|
|
|
|
2020-05-04 21:15:37 +00:00
|
|
|
@classmethod
|
|
|
|
def __setup_mock(cls) -> None:
|
|
|
|
for call in cls.CALLS:
|
|
|
|
setattr(cls, call, fake_call)
|
|
|
|
for tx in cls.TRANSACTIONS:
|
|
|
|
setattr(cls, tx, fake_transaction)
|
|
|
|
|
|
|
|
def __record_tx(self, name: str, params: tuple) -> None:
|
2020-05-04 08:55:59 +00:00
|
|
|
self._SPY_TRANSACTIONS[str(name)].append(params)
|
|
|
|
|
2020-05-04 21:15:37 +00:00
|
|
|
def __record_call(self, name: str, params: tuple) -> None:
|
2020-05-04 08:55:59 +00:00
|
|
|
self._SPY_CALLS[str(name)].append(params)
|
|
|
|
|
|
|
|
def __getattribute__(self, name):
|
|
|
|
"""Spy"""
|
2020-05-04 21:15:37 +00:00
|
|
|
|
2020-05-04 08:55:59 +00:00
|
|
|
get = object.__getattribute__
|
|
|
|
attr = get(self, name)
|
|
|
|
transaction = name in get(self, 'TRANSACTIONS')
|
|
|
|
call = name in get(self, 'CALLS')
|
|
|
|
|
|
|
|
if transaction or call:
|
|
|
|
spy = self.__record_tx if transaction else self.__record_call
|
|
|
|
def wrapped(*args, **kwargs):
|
|
|
|
result = attr(*args, **kwargs)
|
|
|
|
params = args, kwargs
|
|
|
|
spy(name, params)
|
|
|
|
return result
|
|
|
|
return wrapped
|
|
|
|
else:
|
|
|
|
return attr
|
|
|
|
|
2020-05-04 21:15:37 +00:00
|
|
|
#
|
|
|
|
# Utils
|
|
|
|
#
|
|
|
|
|
2020-05-03 01:29:53 +00:00
|
|
|
@classmethod
|
2020-05-04 21:15:37 +00:00
|
|
|
def reset(cls) -> None:
|
|
|
|
cls._SPY_TRANSACTIONS.clear()
|
|
|
|
cls._SPY_CALLS.clear()
|
2020-05-03 01:29:53 +00:00
|
|
|
|
2020-05-04 08:55:59 +00:00
|
|
|
#
|
|
|
|
# Assertions
|
|
|
|
#
|
|
|
|
|
2020-05-04 21:15:37 +00:00
|
|
|
def assert_any_transaction(self) -> None:
|
2020-05-04 08:55:59 +00:00
|
|
|
assert self._SPY_TRANSACTIONS, 'No transactions performed'
|
|
|
|
|
2020-05-04 21:15:37 +00:00
|
|
|
def assert_no_transactions(self) -> None:
|
2020-05-04 08:55:59 +00:00
|
|
|
assert not self._SPY_TRANSACTIONS, 'Transactions performed'
|
|
|
|
|
2020-05-04 21:15:37 +00:00
|
|
|
def assert_only_one_transaction_executed(self) -> None:
|
|
|
|
fail = f"{len(self._SPY_TRANSACTIONS)} were performed ({', '.join(self._SPY_TRANSACTIONS)})."
|
|
|
|
assert len(self._SPY_TRANSACTIONS) == 1, fail
|
2020-05-04 15:37:54 +00:00
|
|
|
|
2020-05-04 21:15:37 +00:00
|
|
|
def assert_transaction_not_called(self, name: str) -> None:
|
|
|
|
assert name not in self._SPY_TRANSACTIONS, f'Unexpected transaction call "{name}".'
|
2020-05-04 15:37:54 +00:00
|
|
|
|
2020-05-04 21:15:37 +00:00
|
|
|
def assert_transaction(self, name: str, call_count: int = 1, **kwargs) -> None:
|
2020-05-04 15:37:54 +00:00
|
|
|
|
|
|
|
# some transaction
|
2020-05-04 08:55:59 +00:00
|
|
|
assert self._SPY_TRANSACTIONS, 'No transactions performed'
|
2020-05-04 21:15:37 +00:00
|
|
|
assert name in self.TRANSACTIONS, f'"{name}" was not performed. Recorded txs: ({" ,".join(self._SPY_TRANSACTIONS)})'
|
2020-05-04 15:37:54 +00:00
|
|
|
|
|
|
|
# this transaction
|
|
|
|
transaction_executions = self._SPY_TRANSACTIONS[name]
|
2020-05-04 21:15:37 +00:00
|
|
|
fail = f'Transaction "{name}" was called an unexpected number of times; ' \
|
|
|
|
f'Expected {call_count} got {len(transaction_executions)}.'
|
|
|
|
assert len(transaction_executions) == call_count, fail
|
2020-05-04 15:37:54 +00:00
|
|
|
|
|
|
|
# transaction params
|
|
|
|
agent_args, agent_kwargs = transaction_executions[0] # use the first occurrence
|
|
|
|
assert kwargs == agent_kwargs, 'Unexpected agent input'
|
|
|
|
|
2020-05-04 21:15:37 +00:00
|
|
|
def assert_contract_calls(self, calls: Tuple[str]) -> None:
|
2020-05-04 15:37:54 +00:00
|
|
|
for call_name in calls:
|
|
|
|
assert call_name in self._SPY_CALLS, f'"{call_name}" was not called'
|
2020-05-04 08:55:59 +00:00
|
|
|
|
2020-05-03 01:29:53 +00:00
|
|
|
|
2020-05-04 21:15:37 +00:00
|
|
|
class MockNucypherToken(MockContractAgent, NucypherTokenAgent):
|
2020-05-05 22:22:24 +00:00
|
|
|
"""Look at me im a token!"""
|
2020-05-04 21:15:37 +00:00
|
|
|
|
|
|
|
|
2020-05-03 01:29:53 +00:00
|
|
|
class MockStakingAgent(MockContractAgent, StakingEscrowAgent):
|
2020-05-05 22:22:24 +00:00
|
|
|
"""dont forget the eggs!"""
|
2020-05-03 01:29:53 +00:00
|
|
|
|
2020-05-04 07:52:03 +00:00
|
|
|
CALLS = ('get_completed_work', )
|
2020-05-02 23:42:14 +00:00
|
|
|
|
|
|
|
|
2020-05-05 22:22:24 +00:00
|
|
|
class MockPolicyManagerAgent(MockContractAgent, PolicyManagerAgent):
|
|
|
|
"""The best ethereum policy manager ever"""
|
|
|
|
|
|
|
|
|
2020-05-02 23:42:14 +00:00
|
|
|
class MockWorkLockAgent(MockContractAgent, WorkLockAgent):
|
|
|
|
|
2020-05-03 01:29:53 +00:00
|
|
|
CALLS = ('check_claim',
|
|
|
|
'eth_to_tokens',
|
|
|
|
'get_deposited_eth',
|
|
|
|
'get_eth_supply',
|
|
|
|
'get_base_deposit_rate',
|
|
|
|
'get_bonus_lot_value',
|
|
|
|
'get_bonus_deposit_rate',
|
|
|
|
'get_bonus_refund_rate',
|
|
|
|
'get_base_refund_rate',
|
|
|
|
'get_completed_work',
|
|
|
|
'get_refunded_work')
|
|
|
|
|
|
|
|
TRANSACTIONS = ('bid',
|
2020-05-02 23:42:14 +00:00
|
|
|
'cancel_bid',
|
|
|
|
'force_refund',
|
|
|
|
'verify_bidding_correctness',
|
|
|
|
'claim',
|
|
|
|
'refund',
|
|
|
|
'withdraw_compensation')
|
2020-05-04 07:52:03 +00:00
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
|
|
# Allow for mocking
|
|
|
|
economics = EconomicsFactory.get_economics(registry=Mock())
|
|
|
|
|
|
|
|
self.ATTRS = {'boosting_refund': economics.worklock_boosting_refund_rate,
|
|
|
|
'slowing_refund': 1, # TODO: another way to get this value?
|
|
|
|
'start_bidding_date': economics.bidding_start_date,
|
|
|
|
'end_bidding_date': economics.bidding_end_date,
|
|
|
|
'end_cancellation_date': economics.cancellation_end_date,
|
|
|
|
'minimum_allowed_bid': economics.worklock_min_allowed_bid,
|
|
|
|
'lot_value': economics.worklock_supply}
|
|
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
|
|
|
|
class MockContractAgency:
|
|
|
|
|
2020-05-05 22:22:24 +00:00
|
|
|
# Test doubles
|
|
|
|
DOUBLE_AGENTS = {NucypherTokenAgent: MockNucypherToken,
|
2020-05-04 21:15:37 +00:00
|
|
|
StakingEscrowAgent: MockStakingAgent,
|
2020-05-05 22:22:24 +00:00
|
|
|
PolicyManagerAgent: MockPolicyManagerAgent,
|
|
|
|
WorkLockAgent: MockWorkLockAgent}
|
|
|
|
|
|
|
|
class NoMockFound(ValueError):
|
|
|
|
"""Well we hadn't made one yet"""
|
2020-05-04 07:52:03 +00:00
|
|
|
|
|
|
|
@classmethod
|
2020-05-04 21:15:37 +00:00
|
|
|
def get_agent(cls, agent_class, *args, **kwargs) -> MockContractAgent:
|
2020-05-04 07:52:03 +00:00
|
|
|
try:
|
|
|
|
double = cls.DOUBLE_AGENTS[agent_class]
|
|
|
|
except KeyError:
|
2020-05-05 22:22:24 +00:00
|
|
|
raise ValueError(f'No mock class available for "{str(agent_class)}"')
|
2020-05-04 07:52:03 +00:00
|
|
|
else:
|
|
|
|
return double()
|
|
|
|
|
|
|
|
@classmethod
|
2020-05-04 21:15:37 +00:00
|
|
|
def get_agent_by_contract_name(cls, contract_name: str, *args, **kwargs) -> MockContractAgent:
|
2020-05-13 14:53:11 +00:00
|
|
|
for agent, test_double in cls.DOUBLE_AGENTS.items():
|
2020-05-04 07:52:03 +00:00
|
|
|
if test_double.registry_contract_name == contract_name:
|
2020-05-13 14:53:11 +00:00
|
|
|
return test_double()
|
2020-05-04 07:52:03 +00:00
|
|
|
else:
|
2020-05-04 21:15:37 +00:00
|
|
|
raise ValueError(f'No mock available for "{contract_name}"')
|