diff --git a/moccasin/__main__.py b/moccasin/__main__.py index cff77a7..66b19bb 100644 --- a/moccasin/__main__.py +++ b/moccasin/__main__.py @@ -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 @@ -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." diff --git a/moccasin/commands/deploy.py b/moccasin/commands/deploy.py new file mode 100644 index 0000000..52d290e --- /dev/null +++ b/moccasin/commands/deploy.py @@ -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 diff --git a/moccasin/commands/test.py b/moccasin/commands/test.py index f71a286..6e91365 100644 --- a/moccasin/commands/test.py +++ b/moccasin/commands/test.py @@ -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", @@ -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( @@ -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) diff --git a/moccasin/config.py b/moccasin/config.py index 723a636..40ab1ce 100644 --- a/moccasin/config.py +++ b/moccasin/config.py @@ -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 diff --git a/moccasin/fixture_tools.py b/moccasin/fixture_tools.py new file mode 100644 index 0000000..eaf7df4 --- /dev/null +++ b/moccasin/fixture_tools.py @@ -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) diff --git a/tests/cli/test_cli_deploy.py b/tests/cli/test_cli_deploy.py new file mode 100644 index 0000000..1668b8b --- /dev/null +++ b/tests/cli/test_cli_deploy.py @@ -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 diff --git a/tests/cli/test_cli_test.py b/tests/cli/test_cli_test.py index 7402433..2545475 100644 --- a/tests/cli/test_cli_test.py +++ b/tests/cli/test_cli_test.py @@ -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 diff --git a/tests/data/complex_project/moccasin.toml b/tests/data/complex_project/moccasin.toml index 11469fd..7f201cb 100644 --- a/tests/data/complex_project/moccasin.toml +++ b/tests/data/complex_project/moccasin.toml @@ -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] diff --git a/tests/data/complex_project/tests/conftest.py b/tests/data/complex_project/tests/conftest.py index 09c31ac..0045116 100644 --- a/tests/data/complex_project/tests/conftest.py +++ b/tests/data/complex_project/tests/conftest.py @@ -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 diff --git a/tests/data/complex_project/tests/test_coffee.py b/tests/data/complex_project/tests/test_coffee.py new file mode 100644 index 0000000..8a9e4e0 --- /dev/null +++ b/tests/data/complex_project/tests/test_coffee.py @@ -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