#!/usr/bin/python3 import itertools import json import os import shutil import sys from copy import deepcopy from pathlib import Path from typing import Dict, List import pytest import solcx from _pytest.monkeypatch import MonkeyPatch from ethpm._utils.ipfs import dummy_ipfs_pin from ethpm.backends.ipfs import BaseIPFSBackend from prompt_toolkit.input.defaults import create_pipe_input import brownie from brownie._cli.console import Console pytest_plugins = "pytester" TARGET_OPTS = { "evm": "evmtester", "pm": "package_test", "plugin": "plugintester", } def pytest_addoption(parser): parser.addoption( "--target", choices=["core", "pm", "plugin"], default="core", help="Target a specific component of the tests.", ) parser.addoption( "--evm", nargs=3, metavar=("solc_versions", "evm_rulesets", "optimizer_runs"), help="Run evm tests against a matrix of solc versions, evm versions, and compiler runs.", ) # remove tests based on config flags and fixture names def pytest_collection_modifyitems(config, items): if config.getoption("--evm"): target = "evm" else: target = config.getoption("--target") for flag, fixture in TARGET_OPTS.items(): if target == flag: continue for test in [i for i in items if fixture in i.fixturenames]: items.remove(test) if target != "core": fixtures = set(TARGET_OPTS.values()) for test in [i for i in items if not fixtures.intersection(i.fixturenames)]: items.remove(test) def pytest_configure(config): if config.getoption("--target") == "plugin" and config.getoption("numprocesses"): raise pytest.UsageError("Cannot use xdist with plugin tests, try adding the '-n 0' flag") if config.getoption("--evm"): # reformat evm options - only do this once to avoid repeat queries for latest solc version solc_versions, evm_verions, runs = [i.split(",") for i in config.option.evm] runs = [int(i) for i in runs] if "latest" in solc_versions: latest_version = solcx.get_available_solc_versions()[0] solc_versions.remove("latest") solc_versions.append(latest_version) config.option.evm = (evm_verions, runs, solc_versions) def pytest_generate_tests(metafunc): # parametrize the evmtester fixture evm_config = metafunc.config.getoption("--evm") if "evmtester" in metafunc.fixturenames and evm_config: params = list(itertools.product(*evm_config)) metafunc.parametrize("evmtester", params, indirect=True) # travis cannot call github ethereum/solidity API, so this method is patched def pytest_sessionstart(): monkeypatch_session = MonkeyPatch() monkeypatch_session.setattr( "solcx.get_available_solc_versions", lambda: [ "v0.6.7", "v0.6.2", "v0.5.15", "v0.5.8", "v0.5.7", "v0.5.0", "v0.4.25", "v0.4.24", "v0.4.22", ], ) # worker ID for xdist process, as an integer @pytest.fixture(scope="session") def xdist_id(worker_id): if worker_id == "master": return 0 return int(worker_id.lstrip("gw")) # ensure a clean data folder, and set unique ganache ports for each xdist worker @pytest.fixture(scope="session", autouse=True) def _base_config(tmp_path_factory, xdist_id): brownie._config.DATA_FOLDER = tmp_path_factory.mktemp(f"data-{xdist_id}") brownie._config._make_data_folders(brownie._config.DATA_FOLDER) cur = brownie.network.state.cur cur.close() cur.connect(brownie._config.DATA_FOLDER.joinpath("pytest.db")) cur.execute("CREATE TABLE IF NOT EXISTS sources (hash PRIMARY KEY, source)") if xdist_id: port = 8545 + xdist_id brownie._config.CONFIG.networks["development"]["cmd_settings"]["port"] = port @pytest.fixture(scope="session") def _project_factory(tmp_path_factory): path = tmp_path_factory.mktemp("base") path.rmdir() shutil.copytree("tests/data/brownie-test-project", path) p = brownie.project.load(path, "TestProject") p.close() return path def _copy_all(src_folder, dest_folder): Path(dest_folder).mkdir(exist_ok=True) for path in Path(src_folder).glob("*"): dest_path = Path(dest_folder).joinpath(path.name) if path.is_dir(): shutil.copytree(path, dest_path) else: shutil.copy(path, dest_path) # project fixtures # creates a temporary folder and sets it as the working directory @pytest.fixture def project(tmp_path): original_path = os.getcwd() os.chdir(tmp_path) yield brownie.project os.chdir(original_path) for p in brownie.project.get_loaded_projects(): p.close(False) # yields a newly initialized Project that is not loaded @pytest.fixture def newproject(project, tmp_path): path = project.new(tmp_path) p = project.load(path, "NewProject") p.close() yield p @pytest.fixture def np_path(newproject): yield newproject._path # copies the tester project into a temporary folder, loads it, and yields a Project object @pytest.fixture def testproject(_project_factory, project, tmp_path): path = tmp_path.joinpath("testproject") _copy_all(_project_factory, path) os.chdir(path) return project.load(path, "TestProject") @pytest.fixture def tp_path(testproject): yield testproject._path @pytest.fixture def otherproject(_project_factory, project, tmp_path): # testproject): _copy_all(_project_factory, tmp_path.joinpath("otherproject")) return project.load(tmp_path.joinpath("otherproject"), "OtherProject") # yields a deployed EVMTester contract # automatically parametrized with multiple compiler versions and settings @pytest.fixture def evmtester(_project_factory, project, tmp_path, accounts, request): evm_version, runs, solc_version = request.param tmp_path.joinpath("contracts").mkdir() shutil.copyfile( _project_factory.joinpath("contracts/EVMTester.sol"), tmp_path.joinpath("contracts/EVMTester.sol"), ) conf_json = { "evm_version": evm_version, "compiler": {"solc": {"version": solc_version, "optimize": runs > 0, "runs": runs}}, } with tmp_path.joinpath("brownie-config.yaml").open("w") as fp: json.dump(conf_json, fp) p = project.load(tmp_path, "EVMProject") return p.EVMTester.deploy({"from": accounts[0]}) @pytest.fixture def plugintesterbase(project, testdir, monkeypatch): brownie.test.coverage.clear() brownie.network.connect() monkeypatch.setattr("brownie.network.connect", lambda k: None) testdir.plugins.extend(["pytest-brownie", "pytest-cov"]) yield testdir brownie.network.disconnect() # setup for pytest-brownie plugin testing @pytest.fixture def plugintester(_project_factory, plugintesterbase, request): _copy_all(_project_factory, plugintesterbase.tmpdir) test_source = getattr(request.module, "test_source", None) if test_source is not None: if isinstance(test_source, str): test_source = [test_source] test_source = {f"tests/test_{i}.py": test_source[i] for i in range(len(test_source))} plugintesterbase.makepyfile(**test_source) yield plugintesterbase # launches and connects to ganache, yields the brownie.network module @pytest.fixture def devnetwork(network, rpc): brownie.network.connect("development") yield brownie.network if rpc.is_active(): rpc.reset() # brownie object fixtures @pytest.fixture def accounts(devnetwork): yield brownie.network.accounts brownie.network.accounts.default = None @pytest.fixture(scope="session") def history(): return brownie.network.history @pytest.fixture def network(): if brownie.network.is_connected(): brownie.network.disconnect(False) yield brownie.network if brownie.network.is_connected(): brownie.network.disconnect(False) @pytest.fixture(scope="session") def rpc(): return brownie.network.rpc @pytest.fixture(scope="session") def web3(): return brownie.network.web3 # configuration fixtures # changes to config or argv are reverted during teardown @pytest.fixture def config(): conf = brownie._config.CONFIG argv = deepcopy(conf.argv) networks = deepcopy(conf.networks) settings = conf.settings._copy() yield conf conf.argv.clear() conf.argv.update(argv) conf.networks.clear() conf.networks.update(networks) conf.settings._unlock() conf.settings.clear() conf.settings.update(settings) conf.settings._lock() @pytest.fixture def argv(): initial = {} initial.update(brownie._config.CONFIG.argv) yield brownie._config.CONFIG.argv brownie._config.CONFIG.argv.clear() brownie._config.CONFIG.argv.update(initial) # cli mode fixtures @pytest.fixture def console_mode(argv): argv["cli"] = "console" @pytest.fixture def test_mode(argv): argv["cli"] = "test" @pytest.fixture def coverage_mode(argv, test_mode): brownie.test.coverage.clear() argv["coverage"] = True argv["always_transact"] = True # contract fixtures @pytest.fixture def BrownieTester(testproject, devnetwork): return testproject.BrownieTester @pytest.fixture def ExternalCallTester(testproject, devnetwork): return testproject.ExternalCallTester @pytest.fixture def tester(BrownieTester, accounts): return BrownieTester.deploy(True, {"from": accounts[0]}) @pytest.fixture def ext_tester(ExternalCallTester, accounts): return ExternalCallTester.deploy({"from": accounts[0]}) @pytest.fixture def vypertester(testproject, devnetwork, accounts): return testproject.VyperTester.deploy({"from": accounts[0]}) # ipfs fixtures class DummyIPFSBackend(BaseIPFSBackend): _assets: Dict = {} _path = Path("./tests/data/ipfs-cache-mock").resolve() def fetch_uri_contents(self, ipfs_uri: str) -> bytes: ipfs_uri = ipfs_uri.replace("ipfs://", "") if ipfs_uri not in self._assets: with self._path.joinpath(ipfs_uri).open() as fp: self._assets[ipfs_uri] = fp.read() return self._assets[ipfs_uri].encode() def pin_assets(self, file_or_dir_path: Path) -> List: """ Return a dict containing the IPFS hash, file name, and size of a file. """ if file_or_dir_path.is_dir(): for path in file_or_dir_path.glob("*"): with path.open() as fp: self._assets[path.name] = fp.read() asset_data = [dummy_ipfs_pin(path) for path in file_or_dir_path.glob("*")] elif file_or_dir_path.is_file(): asset_data = [dummy_ipfs_pin(file_or_dir_path)] with file_or_dir_path.open() as fp: self._assets[file_or_dir_path.name] = fp.read() self._assets[asset_data[0]["Hash"]] = self._assets[file_or_dir_path.name] else: raise FileNotFoundError(f"{file_or_dir_path} is not a valid file or directory path.") return asset_data @pytest.fixture def ipfs_mock(monkeypatch): monkeypatch.setattr("brownie.project.ethpm.InfuraIPFSBackend", DummyIPFSBackend) ipfs_path = brownie._config._get_data_folder().joinpath("ipfs_cache") temp_path = ipfs_path.parent.joinpath("_ipfs_cache") ipfs_path.mkdir(exist_ok=True) ipfs_path.rename(temp_path) yield DummyIPFSBackend() if ipfs_path.exists(): shutil.rmtree(ipfs_path) temp_path.rename(ipfs_path) @pytest.fixture def package_test(): pass # console mock @pytest.fixture(scope="session", autouse=True) def console_setup(): def _exception(obj, *args): obj.resetbuffer() raise sys.exc_info()[0] monkeypatch_session = MonkeyPatch() monkeypatch_session.setattr("brownie._cli.console.Console.showtraceback", _exception) monkeypatch_session.setattr("brownie._cli.console.Console.showsyntaxerror", _exception) Console.prompt_input = create_pipe_input() @pytest.fixture def console(): argv = sys.argv sys.argv = ["brownie", "console"] yield Console sys.argv = argv