Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: auto-fixtures, and new deploy command #46

Merged
merged 2 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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