Skip to content

Commit

Permalink
chore: Jupyter tests (#169)
Browse files Browse the repository at this point in the history
* simplify tests for jupyter plugin
* Run jupyter tests separately with proper dependencies
  • Loading branch information
DanielSchiavini authored Feb 21, 2024
1 parent cf7ed63 commit 50fdb2a
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 206 deletions.
1 change: 1 addition & 0 deletions boa/integrations/jupyter/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def __init__(self, address=None):
Create a BrowserSigner instance.
:param address: The account address. If not provided, it will be requested from the browser.
"""
address = getattr(address, "address", address)
address = _javascript_call(
"loadSigner", address, timeout_message=ADDRESS_TIMEOUT_MESSAGE
)
Expand Down
4 changes: 4 additions & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ pytest
pytest-xdist
pytest-cov
sphinx-rtd-theme

# jupyter
jupyter_server
nest_asyncio
Empty file added tests/__init__.py
Empty file.
72 changes: 9 additions & 63 deletions tests/unitary/jupyter/conftest.py
Original file line number Diff line number Diff line change
@@ -1,74 +1,20 @@
import sys
from multiprocessing.shared_memory import SharedMemory
from unittest.mock import MagicMock

import pytest


@pytest.fixture()
def replace_modules():
mocked_modules = {}

def replace(modules: dict):
for module, mock in modules.items():
assert module not in sys.modules
sys.modules[module] = mock
mocked_modules[module] = mock

yield replace
for m in mocked_modules:
sys.modules.pop(m)


@pytest.fixture()
def tornado_mock(replace_modules):
replace_modules({"tornado.web": MagicMock(authenticated=lambda x: x)})


@pytest.fixture()
def jupyter_module_mock(replace_modules, tornado_mock):
jupyter_mock = MagicMock()
utils = jupyter_mock.utils
serverapp = jupyter_mock.serverapp
base_handlers = jupyter_mock.base.handlers

utils.url_path_join = lambda *args: "/".join(args)
base_handlers.APIHandler = object

replace_modules(
{
"jupyter_server.base.handlers": base_handlers,
"jupyter_server.serverapp": serverapp,
"jupyter_server.utils": utils,
}
)
return jupyter_mock


@pytest.fixture()
def nest_asyncio_mock(replace_modules):
mock = MagicMock()
mock.authenticated = lambda x: x
replace_modules({"nest_asyncio": mock})
return mock
from boa.integrations.jupyter.browser import _generate_token
from boa.integrations.jupyter.constants import SHARED_MEMORY_LENGTH


@pytest.fixture()
def shared_memory_length(nest_asyncio_mock):
from boa.integrations.jupyter.constants import SHARED_MEMORY_LENGTH

return SHARED_MEMORY_LENGTH


@pytest.fixture()
def token(nest_asyncio_mock, jupyter_module_mock):
from boa.integrations.jupyter.browser import _generate_token

def token():
return _generate_token()


@pytest.fixture()
def shared_memory(token, shared_memory_length):
memory = SharedMemory(name=token, create=True, size=shared_memory_length)
yield memory
memory.unlink()
def shared_memory(token):
memory = SharedMemory(name=token, create=True, size=SHARED_MEMORY_LENGTH)
try:
yield memory
finally:
memory.unlink()
170 changes: 48 additions & 122 deletions tests/unitary/jupyter/test_browser.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import json
import re
from asyncio import get_event_loop
Expand All @@ -9,12 +10,8 @@
from eth_account import Account

import boa


@pytest.fixture()
def display_mock(replace_modules):
with mock.patch("boa.integrations.jupyter.browser.display") as display_mock:
yield display_mock
from boa.integrations.jupyter import BrowserSigner
from boa.rpc import RPCError


@pytest.fixture()
Expand All @@ -27,15 +24,8 @@ def mocked_token(token):


@pytest.fixture()
def mock_inject_javascript():
with mock.patch(
"boa.integrations.jupyter.browser.install_jupyter_javascript_triggers"
) as inject_mock:
yield inject_mock


@pytest.fixture()
def env(browser, account, mock_fork):
def env(account, mock_fork, mock_callback):
mock_callback("loadSigner", account.address)
boa.set_browser_env(account)
return boa.env

Expand All @@ -54,6 +44,7 @@ def find_response(mock_calls, func_to_body_dict):
The keys represent either an RPC function or a JS function.
:return: The response to the last call to the display function.
"""
assert mock_calls
(javascript,) = [call for call in mock_calls if call.args][-1].args
js_func, js_args = re.match(
r"window._titanoboa.([a-zA-Z0-9_]+)\(([^)]*)\)", javascript.data
Expand All @@ -76,7 +67,7 @@ def find_response(mock_calls, func_to_body_dict):


@pytest.fixture()
def mock_callback(mocked_token, browser, display_mock, mock_inject_javascript):
def mock_callback(mocked_token, display_mock):
"""Returns a function that allows mocking the result of the frontend callback."""

with mock.patch(
Expand Down Expand Up @@ -104,7 +95,8 @@ def create_task(future):
memory = SharedMemory(name=mocked_token)
memory.buf[0 : len(body)] = body
task = MagicMock()
task.result.return_value = get_event_loop().run_until_complete(future)
loop = get_event_loop()
task.result.return_value = loop.run_until_complete(future)
return task

io_loop.create_task = create_task
Expand All @@ -114,20 +106,16 @@ def create_task(future):

@pytest.fixture()
def mock_fork(mock_callback):
mock_callback("evm_snapshot", "0x123")
mock_callback("evm_revert", "0x123")
mock_callback("evm_snapshot", "0x123456")
mock_callback("evm_revert", "0x12345678")
data = {"number": "0x123", "timestamp": "0x65bbb460"}
mock_callback("eth_getBlockByNumber", data)
yield
boa.reset_env()


@pytest.fixture()
def browser(nest_asyncio_mock, jupyter_module_mock):
# Import the browser module after the mocks have been set up
from boa.integrations.jupyter import browser

return browser
def display_mock():
with patch("boa.integrations.jupyter.browser.display") as display_mock:
yield display_mock


@pytest.fixture()
Expand All @@ -136,151 +124,89 @@ def account():


@pytest.fixture()
def colab_eval_mock(browser):
colab_eval_mock = MagicMock()
with patch.object(browser, "colab_eval_js", colab_eval_mock):
def colab_eval_mock():
with patch("boa.integrations.jupyter.browser.colab_eval_js") as colab_eval_mock:
yield colab_eval_mock


def test_nest_applied(nest_asyncio_mock, browser):
nest_asyncio_mock.apply.assert_called_once()

def test_nest_applied():
assert hasattr(asyncio, "_nest_patched")

def test_browser_signer_given_address(browser, display_mock, mock_inject_javascript):
signer = browser.BrowserSigner("0x1234")
assert signer.address == "0x1234"
display_mock.assert_not_called()
mock_inject_javascript.assert_not_called()


def test_browser_sign_typed_data(
browser, display_mock, mock_inject_javascript, mock_callback
):
signer = browser.BrowserSigner(boa.env.generate_address())
signature = boa.env.generate_address()
def test_browser_sign_typed_data(display_mock, mock_callback, env):
signature = env.generate_address()
mock_callback("signTypedData", signature)
data = signer.sign_typed_data({"name": "My App"}, {"types": []}, {"data": "0x1234"})
data = env.signer.sign_typed_data(
{"name": "My App"}, {"types": []}, {"data": "0x1234"}
)
assert data == signature


def test_browser_signer_no_address(
mocked_token, browser, display_mock, mock_callback, mock_inject_javascript
):
mock_callback("loadSigner", "0x123")
signer = browser.BrowserSigner()
assert signer.address == "0x123"
display_mock.assert_called_once()
(js,), _ = display_mock.call_args
assert f'loadSigner("{mocked_token}")' in js.data
mock_inject_javascript.assert_called()


def test_browser_signer_colab(
colab_eval_mock, mocked_token, browser, mock_inject_javascript, display_mock
):
colab_eval_mock.return_value = json.dumps({"data": "0x123"})
signer = browser.BrowserSigner()
assert signer.address == "0x123"
def test_browser_signer_colab(colab_eval_mock, mocked_token, display_mock):
address = boa.env.generate_address()
colab_eval_mock.return_value = json.dumps({"data": address})
signer = BrowserSigner()
assert signer.address == address
colab_eval_mock.assert_called_once()
(js,), _ = colab_eval_mock.call_args
assert f'loadSigner("{mocked_token}")' in js
mock_inject_javascript.assert_called()
display_mock.assert_not_called()


def test_set_browser_env(
browser,
display_mock,
mock_inject_javascript,
account,
mock_callback,
mock_fork,
env,
):
assert env.eoa == account
assert display_mock.call_count == 4
assert mock_inject_javascript.call_count == 4


def test_browser_loads_signer(
token,
browser,
display_mock,
mock_callback,
mock_inject_javascript,
account,
mock_fork,
):
assert f'loadSigner("{mocked_token}", null)' in js


def test_browser_loads_signer(token, display_mock, mock_callback, account, mock_fork):
mock_callback("loadSigner", account.address)
boa.set_browser_env()
assert boa.env.eoa == account.address
assert (
type(boa.env._accounts[boa.env.eoa]).__name__ == browser.BrowserSigner.__name__
)
mock_inject_javascript.assert_called()
assert type(boa.env._accounts[boa.env.eoa]).__name__ == BrowserSigner.__name__


def test_browser_chain_id(token, env, display_mock, mock_callback):
mock_callback("eth_chainId", "0x123")
assert env.get_chain_id() == "0x123"
display_mock.reset_mock()
mock_callback("eth_chainId", "0x1234")
assert env.get_chain_id() == 4660
mock_callback("wallet_switchEthereumChain")
env.set_chain_id("0x456")
assert display_mock.call_count == 7
env.set_chain_id(1)
assert display_mock.call_count == 3
(js,), _ = display_mock.call_args_list[-2]
assert (
f'rpc("{token}", "wallet_switchEthereumChain", [{{"chainId": "0x456"}}])'
f'rpc("{token}", "wallet_switchEthereumChain", [{{"chainId": "0x1"}}])'
in js.data
)


def test_browser_rpc(
token,
browser,
display_mock,
mock_callback,
mock_inject_javascript,
account,
mock_fork,
env,
):
def test_browser_rpc(token, display_mock, mock_callback, account, mock_fork, env):
mock_callback("eth_gasPrice", "0x123")
assert env.get_gas_price() == 291

assert display_mock.call_count == 5
assert display_mock.call_count == 6
(js,), _ = display_mock.call_args
assert f'rpc("{token}", "eth_gasPrice", [])' in js.data
mock_inject_javascript.assert_called()


def test_browser_rpc_error(
token, browser, display_mock, mock_callback, account, mock_fork, env
):
def test_browser_rpc_error(token, display_mock, mock_callback, account, mock_fork, env):
rpc_error = {"code": -32000, "message": "Reverted"}
mock_callback(
"eth_gasPrice", error={"message": "error", "info": {"error": rpc_error}}
)
with pytest.raises(browser.RPCError) as exc_info:
with pytest.raises(RPCError) as exc_info:
env.get_gas_price()
assert str(exc_info.value) == "-32000: Reverted"


def test_browser_rpc_server_error(
token, browser, display_mock, mock_callback, account, mock_fork, env
token, display_mock, mock_callback, account, mock_fork, env
):
error = {
"code": "UNKNOWN_ERROR",
"error": {"code": -32603, "message": "server error"},
}
mock_callback("eth_gasPrice", error=error)
with pytest.raises(browser.RPCError) as exc_info:
with pytest.raises(RPCError) as exc_info:
env.get_gas_price()
assert str(exc_info.value) == "-32603: server error"


def test_browser_js_error(
token, browser, display_mock, mock_callback, account, mock_fork
):
def test_browser_js_error(token, display_mock, mock_callback, account, mock_fork):
mock_callback("loadSigner", error={"message": "custom message", "stack": ""})
with pytest.raises(browser.RPCError) as exc_info:
browser.BrowserSigner()
with pytest.raises(RPCError) as exc_info:
BrowserSigner()
assert str(exc_info.value) == "CALLBACK_ERROR: custom message"
Loading

0 comments on commit 50fdb2a

Please sign in to comment.