-
-
Notifications
You must be signed in to change notification settings - Fork 131
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
251 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import sys | ||
|
||
import click | ||
import pytest | ||
|
||
|
||
@click.command( | ||
add_help_option=False, # NOTE: This allows pass-through to pytest's help | ||
short_help="Launches pytest and runs the tests for a project", | ||
context_settings=dict(ignore_unknown_options=True), | ||
) | ||
@click.argument("pytest_args", nargs=-1, type=click.UNPROCESSED) | ||
def cli(pytest_args): | ||
return_code = pytest.main([*pytest_args], ["ape_test"]) | ||
if return_code: | ||
# only exit with non-zero status to make testing easier | ||
sys.exit(return_code) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from typing import Optional, Type | ||
|
||
from ape.exceptions import ContractLogicError | ||
|
||
|
||
class RevertsContextManager: | ||
def __init__(self, expected_message: Optional[str] = None): | ||
self.expected_message = expected_message | ||
|
||
def __enter__(self): | ||
pass | ||
|
||
def __exit__(self, exc_type: Type, exc_value: Exception, traceback) -> bool: | ||
if exc_type is None: | ||
raise AssertionError("Transaction did not revert.") | ||
|
||
if not isinstance(exc_value, ContractLogicError): | ||
raise AssertionError( | ||
f"Transaction did not revert.\nHowever, an exception occurred: {exc_value}" | ||
) from exc_value | ||
|
||
actual = exc_value.revert_message | ||
|
||
if self.expected_message is not None and self.expected_message != actual: | ||
raise AssertionError( | ||
f"Expected revert message '{self.expected_message}' but got '{actual}'." | ||
) | ||
|
||
# Returning True causes the expected exception not to get raised | ||
# and the test to pass | ||
return True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
from typing import List | ||
|
||
import pytest | ||
|
||
from ape.api import ProviderAPI, TestAccountAPI | ||
from ape.exceptions import ProviderError | ||
from ape.managers.accounts import AccountManager | ||
from ape.managers.networks import NetworkManager | ||
from ape.managers.project import ProjectManager | ||
|
||
|
||
class PytestApeFixtures: | ||
def __init__(self, accounts: AccountManager, networks: NetworkManager, project: ProjectManager): | ||
self._accounts = accounts | ||
self._networks = networks | ||
self._project = project | ||
|
||
@pytest.fixture | ||
def accounts(self, provider) -> List[TestAccountAPI]: | ||
return self._accounts.test_accounts | ||
|
||
@pytest.fixture | ||
def provider(self) -> ProviderAPI: | ||
active_provider = self._networks.active_provider | ||
|
||
if active_provider is None: | ||
raise ProviderError("Provider is not set.") | ||
|
||
return active_provider | ||
|
||
@pytest.fixture(scope="session") | ||
def project(self) -> ProjectManager: | ||
return self._project |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import sys | ||
from pathlib import Path | ||
|
||
import pytest | ||
|
||
from ape import accounts, networks, project | ||
from ape_test.fixtures import PytestApeFixtures | ||
from ape_test.runners import PytestApeRunner | ||
|
||
|
||
def pytest_addoption(parser): | ||
parser.addoption( | ||
"--showinternal", | ||
action="store_true", | ||
) | ||
parser.addoption( | ||
"--network", | ||
action="store", | ||
default=networks.default_ecosystem.name, | ||
help="Override the default network and provider. (see ``ape networks list`` for options)", | ||
) | ||
# NOTE: Other testing plugins, such as hypothesis, should integrate with pytest separately | ||
|
||
|
||
def pytest_configure(config): | ||
# Do not include ape internals in tracebacks unless explicitly asked | ||
if not config.getoption("showinternal"): | ||
base_path = Path(sys.modules["ape"].__file__).parent.as_posix() | ||
|
||
def is_module(v): | ||
return getattr(v, "__file__", None) and v.__file__.startswith(base_path) | ||
|
||
modules = [v for v in sys.modules.values() if is_module(v)] | ||
for module in modules: | ||
module.__tracebackhide__ = True | ||
|
||
# Enable verbose output if stdout capture is disabled | ||
config.option.verbose = config.getoption("capture") == "no" | ||
|
||
# Inject the runner plugin (must happen before fixtures registration) | ||
session = PytestApeRunner(config, project, networks) | ||
config.pluginmanager.register(session, "ape-test") | ||
|
||
# Inject fixtures | ||
fixtures = PytestApeFixtures(accounts, networks, project) | ||
config.pluginmanager.register(fixtures, "ape-fixtures") | ||
|
||
|
||
def pytest_load_initial_conftests(early_config): | ||
""" | ||
Compile contracts before loading conftests. | ||
""" | ||
cap_sys = early_config.pluginmanager.get_plugin("capturemanager") | ||
if not project.sources_missing: | ||
# Suspend stdout capture to display compilation data | ||
cap_sys.suspend() | ||
try: | ||
project.load_contracts() | ||
except Exception as err: | ||
raise pytest.UsageError(f"Unable to load project. Reason: {err}") | ||
finally: | ||
cap_sys.resume() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
from typing import Optional | ||
|
||
import pytest | ||
|
||
import ape | ||
from ape.api import TestProviderAPI | ||
from ape.logging import logger | ||
|
||
from .contextmanagers import RevertsContextManager | ||
|
||
|
||
class PytestApeRunner: | ||
def __init__(self, config, project, networks): | ||
self.config = config | ||
self.project = project | ||
self.networks = networks | ||
self._warned_for_missing_features = False | ||
ape.reverts = RevertsContextManager | ||
|
||
@property | ||
def _network_choice(self) -> str: | ||
""" | ||
The option the user providers via --network (or the default). | ||
""" | ||
return self.config.getoption("network") | ||
|
||
@property | ||
def _provider(self) -> Optional[TestProviderAPI]: | ||
""" | ||
The active provider. | ||
""" | ||
return self.networks.active_provider | ||
|
||
@pytest.hookimpl(hookwrapper=True) | ||
def pytest_runtest_protocol(self, item, nextitem): | ||
snapshot_id = None | ||
|
||
# Try to snapshot if the provider supported it. | ||
if hasattr(self._provider, "snapshot"): | ||
try: | ||
snapshot_id = self._provider.snapshot() | ||
except NotImplementedError: | ||
self._warn_for_unimplemented_snapshot() | ||
pass | ||
else: | ||
self._warn_for_unimplemented_snapshot() | ||
|
||
yield | ||
|
||
# Try to revert to the state before the test began. | ||
if snapshot_id: | ||
self._provider.revert(snapshot_id) | ||
|
||
def _warn_for_unimplemented_snapshot(self): | ||
if self._warned_for_missing_features: | ||
return | ||
|
||
logger.warning( | ||
"The connected provider does not support snapshotting. " | ||
"Tests will not be completely isolated." | ||
) | ||
self._warned_for_missing_features = True | ||
|
||
def pytest_sessionstart(self): | ||
""" | ||
Called after the `Session` object has been created and before performing | ||
collection and entering the run test loop. | ||
Removes `PytestAssertRewriteWarning` warnings from the terminalreporter. | ||
This prevents warnings that "the `ape` library was already imported and | ||
so related assertions cannot be rewritten". The warning is not relevant | ||
for end users who are performing tests with ape. | ||
""" | ||
reporter = self.config.pluginmanager.get_plugin("terminalreporter") | ||
warnings = reporter.stats.pop("warnings", []) | ||
warnings = [i for i in warnings if "PytestAssertRewriteWarning" not in i.message] | ||
if warnings and not self.config.getoption("--disable-warnings"): | ||
reporter.stats["warnings"] = warnings | ||
|
||
@pytest.hookimpl(trylast=True, hookwrapper=True) | ||
def pytest_collection_finish(self, session): | ||
""" | ||
Called after collection has been performed and modified. | ||
""" | ||
outcome = yield | ||
|
||
# Only start provider if collected tests. | ||
if not outcome.get_result() and session.items and not self.networks.active_provider: | ||
self.networks.active_provider = self.networks.get_provider_from_choice( | ||
self._network_choice | ||
) | ||
self.networks.active_provider.connect() | ||
|
||
def pytest_sessionfinish(self): | ||
""" | ||
Called after whole test run finished, right before returning the exit | ||
status to the system. | ||
""" | ||
if self._provider is not None: | ||
self._provider.disconnect() |