Skip to content

Commit

Permalink
perf: Ape's pytest plugin performance improvements (#2361)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Oct 31, 2024
1 parent ebb6473 commit 437d326
Show file tree
Hide file tree
Showing 6 changed files with 40 additions and 38 deletions.
13 changes: 9 additions & 4 deletions src/ape/pytest/config.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
from functools import cached_property
from typing import TYPE_CHECKING, Any, Optional, Union

from ape.types.trace import ContractFunctionPath
from ape.utils.basemodel import ManagerAccessMixin

if TYPE_CHECKING:
from _pytest.config import Config as PytestConfig

from ape.types.trace import ContractFunctionPath


def _get_config_exclusions(config) -> list["ContractFunctionPath"]:
from ape.types.trace import ContractFunctionPath

def _get_config_exclusions(config) -> list[ContractFunctionPath]:
return [
ContractFunctionPath(contract_name=x.contract_name, method_name=x.method_name)
for x in config.exclude
Expand Down Expand Up @@ -74,10 +77,12 @@ def show_internal(self) -> bool:
return self.pytest_config.getoption("--show-internal")

@cached_property
def gas_exclusions(self) -> list[ContractFunctionPath]:
def gas_exclusions(self) -> list["ContractFunctionPath"]:
"""
The combination of both CLI values and config values.
"""
from ape.types.trace import ContractFunctionPath

cli_value = self.pytest_config.getoption("--gas-exclude")
exclusions = (
[ContractFunctionPath.from_str(item) for item in cli_value.split(",")]
Expand All @@ -89,7 +94,7 @@ def gas_exclusions(self) -> list[ContractFunctionPath]:
return exclusions

@cached_property
def coverage_exclusions(self) -> list[ContractFunctionPath]:
def coverage_exclusions(self) -> list["ContractFunctionPath"]:
return _get_config_exclusions(self.ape_test_config.coverage)

def get_pytest_plugin(self, name: str) -> Optional[Any]:
Expand Down
4 changes: 3 additions & 1 deletion src/ape/pytest/contextmanagers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from ethpm_types.abi import ErrorABI

from ape.contracts import ContractInstance
from ape.exceptions import ContractLogicError, CustomError, TransactionError
from ape.utils.basemodel import ManagerAccessMixin

Expand Down Expand Up @@ -105,6 +104,9 @@ def _check_expected_message(self, exception: ContractLogicError):
raise AssertionError(f"{assertion_error_prefix} but got '{actual}'.")

def _check_custom_error(self, exception: Union[CustomError]):
# perf: avoid loading from contracts namespace until needed.
from ape.contracts import ContractInstance

expected_error_cls = self.expected_message

if not isinstance(expected_error_cls, ErrorABI) and not isinstance(
Expand Down
12 changes: 7 additions & 5 deletions src/ape/pytest/coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import click

from ape.logging import logger
from ape.types.coverage import CoverageProject, CoverageReport
from ape.utils.basemodel import ManagerAccessMixin
from ape.utils.misc import get_current_timestamp_ms
from ape.utils.os import get_full_extension, get_relative_path
Expand All @@ -17,6 +16,7 @@

from ape.managers.project import ProjectManager
from ape.pytest.config import ConfigWrapper
from ape.types.coverage import CoverageReport
from ape.types.trace import ContractFunctionPath, ControlFlow, SourceTraceback


Expand All @@ -30,7 +30,7 @@ def __init__(
self._sources: Union[
Iterable["ContractSource"], Callable[[], Iterable["ContractSource"]]
] = sources
self._report: Optional[CoverageReport] = None
self._report: Optional["CoverageReport"] = None

@property
def sources(self) -> list["ContractSource"]:
Expand All @@ -45,7 +45,7 @@ def sources(self) -> list["ContractSource"]:
return self._sources

@property
def report(self) -> CoverageReport:
def report(self) -> "CoverageReport":
if self._report is None:
self._report = self._init_coverage_profile()

Expand All @@ -57,7 +57,9 @@ def reset(self):

def _init_coverage_profile(
self,
) -> CoverageReport:
) -> "CoverageReport":
from ape.types.coverage import CoverageProject, CoverageReport

# source_id -> pc(s) -> times hit
project_coverage = CoverageProject(name=self.project.name or "__local__")

Expand Down Expand Up @@ -161,7 +163,7 @@ def __init__(

@property
def data(self) -> Optional[CoverageData]:
if not self.config_wrapper.track_coverage:
if not self.enabled:
return None

elif self._data is None:
Expand Down
5 changes: 2 additions & 3 deletions src/ape/pytest/gas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from evm_trace.gas import merge_reports

from ape.types.trace import GasReport
from ape.utils.basemodel import ManagerAccessMixin
from ape.utils.trace import _exclude_gas, parse_gas_table

Expand All @@ -13,7 +12,7 @@
from ape.api.trace import TraceAPI
from ape.pytest.config import ConfigWrapper
from ape.types.address import AddressType
from ape.types.trace import ContractFunctionPath
from ape.types.trace import ContractFunctionPath, GasReport


class GasTracker(ManagerAccessMixin):
Expand All @@ -24,7 +23,7 @@ class GasTracker(ManagerAccessMixin):

def __init__(self, config_wrapper: "ConfigWrapper"):
self.config_wrapper = config_wrapper
self.session_gas_report: Optional[GasReport] = None
self.session_gas_report: Optional["GasReport"] = None

@property
def enabled(self) -> bool:
Expand Down
40 changes: 16 additions & 24 deletions src/ape/pytest/plugin.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,7 @@
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Optional

from ape.exceptions import ConfigError
from ape.pytest.config import ConfigWrapper
from ape.pytest.coverage import CoverageTracker
from ape.pytest.fixtures import PytestApeFixtures, ReceiptCapture
from ape.pytest.gas import GasTracker
from ape.pytest.runners import PytestApeRunner
from ape.utils.basemodel import ManagerAccessMixin

if TYPE_CHECKING:
from ape.api.networks import EcosystemAPI


def _get_default_network(ecosystem: Optional["EcosystemAPI"] = None) -> str:
if ecosystem is None:
ecosystem = ManagerAccessMixin.network_manager.default_ecosystem

return ecosystem.name


def pytest_addoption(parser):
Expand All @@ -40,7 +23,6 @@ def add_option(*names, **kwargs):
add_option(
"--network",
action="store",
default=_get_default_network(),
help="Override the default network and provider (see ``ape networks list`` for options).",
)
add_option(
Expand All @@ -64,7 +46,7 @@ def add_option(*names, **kwargs):
action="store",
help="A comma-separated list of contract:method-name glob-patterns to ignore.",
)
parser.addoption("--coverage", action="store_true", help="Collect contract coverage.")
add_option("--coverage", action="store_true", help="Collect contract coverage.")

# NOTE: Other pytest plugins, such as hypothesis, should integrate with pytest separately

Expand All @@ -86,17 +68,27 @@ def is_module(v):
except AttributeError:
pass

config_wrapper = ConfigWrapper(config)
receipt_capture = ReceiptCapture(config_wrapper)
gas_tracker = GasTracker(config_wrapper)
coverage_tracker = CoverageTracker(config_wrapper)

if not config.option.verbose:
# Enable verbose output if stdout capture is disabled
config.option.verbose = config.getoption("capture") == "no"
# else: user has already changes verbosity to an equal or higher level; avoid downgrading.

if "--help" in config.invocation_params.args:
# perf: Don't bother setting up runner if only showing help.
return

from ape.pytest.config import ConfigWrapper
from ape.pytest.coverage import CoverageTracker
from ape.pytest.fixtures import PytestApeFixtures, ReceiptCapture
from ape.pytest.gas import GasTracker
from ape.pytest.runners import PytestApeRunner
from ape.utils.basemodel import ManagerAccessMixin

# Register the custom Ape test runner
config_wrapper = ConfigWrapper(config)
receipt_capture = ReceiptCapture(config_wrapper)
gas_tracker = GasTracker(config_wrapper)
coverage_tracker = CoverageTracker(config_wrapper)
runner = PytestApeRunner(config_wrapper, receipt_capture, gas_tracker, coverage_tracker)
config.pluginmanager.register(runner, "ape-test")

Expand Down
4 changes: 3 additions & 1 deletion src/ape/pytest/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from ape.exceptions import ConfigError
from ape.logging import LogLevel
from ape.utils.basemodel import ManagerAccessMixin
from ape_console._cli import console

if TYPE_CHECKING:
from ape.api.networks import ProviderContextManager
Expand Down Expand Up @@ -90,6 +89,8 @@ def pytest_exception_interact(self, report, call):
)

if self.config_wrapper.interactive and report.failed:
from ape_console._cli import console

traceback = call.excinfo.traceback[-1]

# Suspend capsys to ignore our own output.
Expand Down Expand Up @@ -124,6 +125,7 @@ def pytest_exception_interact(self, report, call):
click.echo("Starting interactive mode. Type `exit` to halt current test.")

namespace = {"_callinfo": call, **globals_dict, **locals_dict}

console(extra_locals=namespace, project=self.local_project, embed=True)

if capman:
Expand Down

0 comments on commit 437d326

Please sign in to comment.