Skip to content

Commit

Permalink
feat: auto-fixtures, and new deploy command (#46)
Browse files Browse the repository at this point in the history
* feat: auto-fixtures, and new deploy command

* fix: typecheck
  • Loading branch information
PatrickAlphaC authored Sep 15, 2024
1 parent dc39b8b commit 941e454
Show file tree
Hide file tree
Showing 10 changed files with 209 additions and 23 deletions.
56 changes: 38 additions & 18 deletions moccasin/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,28 +221,24 @@ def generate_main_parser_and_sub_parsers() -> (
default="./script/deploy.py",
)
add_network_args_to_parser(run_parser)
add_account_args_to_parser(run_parser)

key_or_account_group = run_parser.add_mutually_exclusive_group()
key_or_account_group.add_argument(
"--account", help="Keystore account you want to use.", type=str
# ------------------------------------------------------------------
# DEPLOY COMMAND
# ------------------------------------------------------------------
deploy_parser = sub_parsers.add_parser(
"deploy",
help="Deploys a contract named in the config with a deploy script.",
description="Deploys a contract named in the config with a deploy script.",
parents=[parent_parser],
)
key_or_account_group.add_argument(
"--private-key",
help="Private key you want to use to get an unlocked account.",
deploy_parser.add_argument(
"contract_name",
help=f"Name of contract in your {CONFIG_NAME} to deploy.",
type=str,
)

password_group = run_parser.add_mutually_exclusive_group()
password_group.add_argument(
"--password",
help="Password for the keystore account.",
action=RequirePasswordAction,
)
password_group.add_argument(
"--password-file-path",
help="Path to the file containing the password for the keystore account.",
action=RequirePasswordAction,
)
add_network_args_to_parser(deploy_parser)
add_account_args_to_parser(deploy_parser)

# ------------------------------------------------------------------
# WALLET COMMAND
Expand Down Expand Up @@ -469,6 +465,30 @@ def generate_main_parser_and_sub_parsers() -> (
# ------------------------------------------------------------------
# HELPER FUNCTIONS
# ------------------------------------------------------------------
def add_account_args_to_parser(parser: argparse.ArgumentParser):
key_or_account_group = parser.add_mutually_exclusive_group()
key_or_account_group.add_argument(
"--account", help="Keystore account you want to use.", type=str
)
key_or_account_group.add_argument(
"--private-key",
help="Private key you want to use to get an unlocked account.",
type=str,
)

password_group = parser.add_mutually_exclusive_group()
password_group.add_argument(
"--password",
help="Password for the keystore account.",
action=RequirePasswordAction,
)
password_group.add_argument(
"--password-file-path",
help="Path to the file containing the password for the keystore account.",
action=RequirePasswordAction,
)


def add_network_args_to_parser(parser: argparse.ArgumentParser):
parser.add_argument(
"--fork", action="store_true", help="If you want to fork the RPC."
Expand Down
34 changes: 34 additions & 0 deletions moccasin/commands/deploy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from moccasin.logging import logger
from moccasin.config import get_config, initialize_global_config
from moccasin._sys_path_and_config_setup import (
_patch_sys_path,
_setup_network_and_account_from_args,
)
from argparse import Namespace


def main(args: Namespace) -> int:
initialize_global_config()
config_contracts = get_config().contracts_folder
config_root = get_config().get_root()

# Set up the environment (add necessary paths to sys.path, etc.)
with _patch_sys_path([config_root, config_root / config_contracts]):
_setup_network_and_account_from_args(
network=args.network,
url=args.url,
fork=args.fork,
account=args.account,
private_key=args.private_key,
password=args.password,
password_file_path=args.password_file_path,
)
config = get_config()
active_network = config.get_active_network()
deployed_contract = active_network.get_or_deploy_contract(
args.contract_name, force_deploy=True
)
logger.info(
f"Deployed contract {args.contract_name} on {active_network.name} to {deployed_contract.address}"
)
return 0
12 changes: 11 additions & 1 deletion moccasin/commands/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import sys
from argparse import Namespace

from moccasin.constants.vars import TESTS_FOLDER

PYTEST_ARGS: list[str] = [
"file_or_dir",
"k",
Expand Down Expand Up @@ -55,7 +57,7 @@ def main(args: Namespace) -> int:
def _run_project_tests(pytest_args: List[str], network: str = None, fork: bool = False):
config = get_config()
config_root = config.get_root()
test_path = "test"
test_path = TESTS_FOLDER

with _patch_sys_path([config_root, config_root / test_path]):
_setup_network_and_account_from_args(
Expand All @@ -67,6 +69,14 @@ def _run_project_tests(pytest_args: List[str], network: str = None, fork: bool =
password=None,
password_file_path=None,
)

pytest_args = [
"--confcutdir",
str(config_root),
"--rootdir",
str(config_root),
] + pytest_args

return_code: int = pytest.main(["--assert=plain"] + pytest_args)
if return_code:
sys.exit(return_code)
3 changes: 3 additions & 0 deletions moccasin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,9 @@ def _get_abi_from_params(
abi = boa_get_abi_from_explorer(str(address), quiet=True)
return abi # type: ignore

def get_named_contract(self, contract_name: str) -> NamedContract | None:
return self.contracts.get(contract_name, None)

@property
def alias(self) -> str:
return self.name
Expand Down
71 changes: 71 additions & 0 deletions moccasin/fixture_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from types import ModuleType
from typing import Callable, Literal, cast
from pytest import fixture
from moccasin.config import get_config
from boa.contracts.vyper.vyper_contract import VyperContract
from boa.contracts.abi.abi_contract import ABIContract
import inspect


ScopeType = Literal["function", "class", "module", "package", "session"]


def request_fixtures(
fixture_requests: list[str | tuple[str, str]], scope: str = "module"
):
# Dear Charles, don't kill me. Idk how this works.
current_frame = inspect.currentframe()
if current_frame is None or current_frame.f_back is None:
raise RuntimeError("Cannot determine caller module")
caller_frame = current_frame.f_back

caller_module = inspect.getmodule(caller_frame)
if caller_module is None:
raise RuntimeError("Cannot determine caller module")
module: ModuleType = caller_module
for fixture_request in fixture_requests:
if isinstance(fixture_request, tuple):
named_contract_name, fixture_name = fixture_request
else:
named_contract_name = fixture_request
fixture_name = named_contract_name
request_fixture(module, named_contract_name, fixture_name, scope)


def request_fixture(
module: ModuleType,
named_contract_name: str,
fixture_name: str,
scope: str = "module",
):
active_network = get_config().get_active_network()
named_contract = active_network.get_named_contract(named_contract_name)
if named_contract is None:
raise ValueError(
f"No contract found for contract '{named_contract_name}' on network {active_network.name}"
)
if named_contract.deployer_script is None:
raise ValueError(
f"No deploy function found for '{named_contract_name}' on network {active_network.name}"
)

def deploy_func() -> VyperContract | ABIContract:
return active_network.get_or_deploy_contract(named_contract_name)

# Create the fixture function
fixture_function = make_fixture(deploy_func, fixture_name, cast(ScopeType, scope))

# Add the fixture to the module's namespace
setattr(module, fixture_name, fixture_function)


def make_fixture(
deploy_func: Callable[[], VyperContract | ABIContract],
fixture_name: str,
scope: Literal["function", "class", "module", "package", "session"],
) -> Callable[[], VyperContract | ABIContract]:
@fixture(scope=scope, name=fixture_name)
def fixture_func(deploy_func=deploy_func):
return deploy_func()

return cast(Callable[[], VyperContract | ABIContract], fixture_func)
40 changes: 40 additions & 0 deletions tests/cli/test_cli_deploy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from pathlib import Path
import subprocess
import os
from tests.conftest import COMPLEX_PROJECT_PATH


# --------------------------------------------------------------
# WITHOUT ANVIL
# --------------------------------------------------------------
def test_deploy_price_feed_pyevm(mox_path):
current_dir = Path.cwd()
try:
os.chdir(COMPLEX_PROJECT_PATH)
result = subprocess.run(
[mox_path, "deploy", "price_feed"],
check=True,
capture_output=True,
text=True,
)
finally:
os.chdir(current_dir)
assert "Deployed contract price_feed on pyevm to" in result.stderr


# --------------------------------------------------------------
# WITH ANVIL
# --------------------------------------------------------------
def test_deploy_price_feed_anvil(mox_path, anvil_process):
current_dir = Path.cwd()
try:
os.chdir(COMPLEX_PROJECT_PATH)
result = subprocess.run(
[mox_path, "deploy", "price_feed", "--network", "anvil"],
check=True,
capture_output=True,
text=True,
)
finally:
os.chdir(current_dir)
assert "Deployed contract price_feed on anvil to" in result.stderr
4 changes: 2 additions & 2 deletions tests/cli/test_cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def test_test_complex_project_passes_pytest_flags(complex_cleanup_out_folder, mo
)
finally:
os.chdir(current_dir)
assert "2 passed" not in result.stdout
assert "4 passed" not in result.stdout
assert "1 passed" in result.stdout
assert "2 deselected" in result.stdout
assert "4 deselected" in result.stdout
assert result.returncode == 0
2 changes: 0 additions & 2 deletions tests/data/complex_project/moccasin.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ explorer_api_key = "${ETHERSCAN_API_KEY}"
save_abi_path = "abis"

[networks.contracts]
# TODO: Make deployer_script be a python module or a file name
# Example: "deployer_script" or "foo/deployer_script.py" not "foo/deployer_script" (cuz it looks like a dir)
price_feed = { abi_from_file_path = "mocks/MockV3Aggregator.vy", force_deploy = false, deployer_script = "mock_deployer/deploy_feed", fixture = false }

[networks.optimism]
Expand Down
3 changes: 3 additions & 0 deletions tests/data/complex_project/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import pytest
from script.deploy import deploy
from moccasin.fixture_tools import request_fixtures

request_fixtures(["price_feed", ("price_feed", "eth_usd")], scope="session")


@pytest.fixture
Expand Down
7 changes: 7 additions & 0 deletions tests/data/complex_project/tests/test_coffee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
def test_using_fixture_one(price_feed):
assert price_feed.address is not None


def test_using_fixture_two(price_feed, eth_usd):
assert price_feed.address is not None
assert eth_usd.address is not None

0 comments on commit 941e454

Please sign in to comment.