Skip to content

Commit

Permalink
fix: issue where runner injected at wrong spot
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey committed Sep 11, 2024
1 parent 37c7171 commit c590099
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 122 deletions.
57 changes: 31 additions & 26 deletions src/ape/pytest/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,34 +137,10 @@ def pytest_runtest_setup(self, item):
or (hasattr(pytest, "DoctestItem") and isinstance(item, pytest.DoctestItem))
or "_function_isolation" in item.fixturenames # prevent double injection
):
# isolation is disabled via cmdline option
# isolation is disabled via cmdline option or running doc-tests.
return

fixture_map = item.session._fixturemanager._arg2fixturedefs
scopes = [
definition.scope
for name, definitions in fixture_map.items()
if name in item.fixturenames
for definition in definitions
]

for scope in ("session", "package", "module", "class"):
# iterate through scope levels and insert the isolation fixture
# prior to the first fixture with that scope
try:
idx = scopes.index(scope) # will raise ValueError if `scope` not found
item.fixturenames.insert(idx, f"_{scope}_isolation")
scopes.insert(idx, scope)
except ValueError:
# intermediate scope isolations aren't filled in
continue

# insert function isolation by default
try:
item.fixturenames.insert(scopes.index("function"), "_function_isolation")
except ValueError:
# no fixtures with function scope, so append function isolation
item.fixturenames.append("_function_isolation")
_insert_isolation_fixtures(item)

def pytest_sessionstart(self):
"""
Expand Down Expand Up @@ -272,3 +248,32 @@ def pytest_unconfigure(self):
self.chain_manager.contracts.clear_local_caches()
self.gas_tracker.session_gas_report = None
self.coverage_tracker.reset()


def _insert_isolation_fixtures(item):
# Abstracted for testing purposes.
fixture_map = item.session._fixturemanager._arg2fixturedefs
scopes = [
definition.scope
for name, definitions in fixture_map.items()
if name in item.fixturenames
for definition in definitions
]

for scope in ("session", "package", "module", "class"):
# iterate through scope levels and insert the isolation fixture
# prior to the first fixture with that scope
try:
idx = scopes.index(scope) # will raise ValueError if `scope` not found
item.fixturenames.append(f"_{scope}_isolation")
scopes.insert(idx, scope)
except ValueError:
# intermediate scope isolations aren't filled in
continue

# insert function isolation by default
try:
item.fixturenames.insert(scopes.index("function"), "_function_isolation")
except ValueError:
# no fixtures with function scope, so append function isolation
item.fixturenames.append("_function_isolation")
95 changes: 95 additions & 0 deletions tests/functional/test_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,98 @@ def test_isolation_restore_not_implemented(mocker, networks, fixtures):

finally:
networks.active_provider = orig_provider


class TestPytestApeFixtures:
@pytest.fixture
def mock_config_wrapper(self, mocker):
return mocker.MagicMock()

@pytest.fixture
def mock_receipt_capture(self, mocker):
return mocker.MagicMock()

@pytest.fixture
def mock_evm(self, mocker):
return mocker.MagicMock()

@pytest.fixture
def fixtures(self, mock_config_wrapper, mock_receipt_capture):
return PytestApeFixtures(mock_config_wrapper, mock_receipt_capture)

@pytest.fixture
def use_mock_provider(self, networks, mock_provider, mock_evm):
mock_provider._web3.eth.get_block.return_value = {
"timestamp": 123,
"gasLimit": 0,
"gasUsed": 0,
"number": 0,
}
orig_provider = networks.active_provider
networks.active_provider = mock_provider
mock_provider._evm_backend = mock_evm
yield mock_provider
networks.active_provider = orig_provider

@pytest.mark.parametrize("snapshot_id", (0, 1, "123"))
def test_isolation(self, snapshot_id, networks, use_mock_provider, fixtures, mock_evm):
mock_evm.take_snapshot.return_value = snapshot_id
isolation_context = fixtures._isolation()
next(isolation_context) # Enter.
assert mock_evm.take_snapshot.call_count == 1
assert mock_evm.revert_to_snapshot.call_count == 0
next(isolation_context, None) # Exit.
mock_evm.revert_to_snapshot.assert_called_once_with(snapshot_id)

def test_isolation_when_snapshot_fails_avoids_restore(
self, networks, use_mock_provider, fixtures, mock_evm
):
mock_evm.take_snapshot.side_effect = NotImplementedError
isolation_context = fixtures._isolation()
next(isolation_context) # Enter.
assert mock_evm.take_snapshot.call_count == 1
assert mock_evm.revert_to_snapshot.call_count == 0
next(isolation_context, None) # Exit.
# It doesn't even try!
assert mock_evm.revert_to_snapshot.call_count == 0

def test_isolation_restore_fails_avoids_snapshot_next_time(
self, networks, use_mock_provider, fixtures, mock_evm
):
mock_evm.take_snapshot.return_value = 123
mock_evm.revert_to_snapshot.side_effect = NotImplementedError
isolation_context = fixtures._isolation()
next(isolation_context) # Enter.
# Snapshot works, we get this far.
assert mock_evm.take_snapshot.call_count == 1
assert mock_evm.revert_to_snapshot.call_count == 0

# At this point, it is realized snapshotting is no-go.
mock_evm.take_snapshot.reset_mock()
next(isolation_context, None) # Exit.
isolation_context = fixtures._isolation()
next(isolation_context) # Enter again.
# This time, snapshotting is NOT attempted.
assert mock_evm.take_snapshot.call_count == 0

def test_isolation_supports_flag_set_after_successful_snapshot(
self, networks, use_mock_provider, fixtures, mock_evm
):
"""
Testing the unusual case where `._supports_snapshot` was changed manually after
a successful snapshot and before the restore attempt.
"""
mock_evm.take_snapshot.return_value = 123
isolation_context = fixtures._isolation()
next(isolation_context) # Enter.
assert mock_evm.take_snapshot.call_count == 1
assert mock_evm.revert_to_snapshot.call_count == 0

# HACK: Change the flag manually to show it will avoid
# the restore.
fixtures._supports_snapshot = False

next(isolation_context, None) # Exit.
# Even though snapshotting worked, the flag was changed,
# and so the restore never gets attempted.
assert mock_evm.revert_to_snapshot.call_count == 0
128 changes: 32 additions & 96 deletions tests/functional/test_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import pytest

from ape.pytest.fixtures import PytestApeFixtures
from ape.pytest.runners import _insert_isolation_fixtures
from ape_test import ApeTestConfig


Expand All @@ -14,96 +12,34 @@ def test_balance_set_from_currency_str(self):
assert actual == expected


class TestPytestApeFixtures:
@pytest.fixture
def mock_config_wrapper(self, mocker):
return mocker.MagicMock()

@pytest.fixture
def mock_receipt_capture(self, mocker):
return mocker.MagicMock()

@pytest.fixture
def mock_evm(self, mocker):
return mocker.MagicMock()

@pytest.fixture
def fixtures(self, mock_config_wrapper, mock_receipt_capture):
return PytestApeFixtures(mock_config_wrapper, mock_receipt_capture)

@pytest.fixture
def use_mock_provider(self, networks, mock_provider, mock_evm):
mock_provider._web3.eth.get_block.return_value = {
"timestamp": 123,
"gasLimit": 0,
"gasUsed": 0,
"number": 0,
}
orig_provider = networks.active_provider
networks.active_provider = mock_provider
mock_provider._evm_backend = mock_evm
yield mock_provider
networks.active_provider = orig_provider

@pytest.mark.parametrize("snapshot_id", (0, 1, "123"))
def test_isolation(self, snapshot_id, networks, use_mock_provider, fixtures, mock_evm):
mock_evm.take_snapshot.return_value = snapshot_id
isolation_context = fixtures._isolation()
next(isolation_context) # Enter.
assert mock_evm.take_snapshot.call_count == 1
assert mock_evm.revert_to_snapshot.call_count == 0
next(isolation_context, None) # Exit.
mock_evm.revert_to_snapshot.assert_called_once_with(snapshot_id)

def test_isolation_when_snapshot_fails_avoids_restore(
self, networks, use_mock_provider, fixtures, mock_evm
):
mock_evm.take_snapshot.side_effect = NotImplementedError
isolation_context = fixtures._isolation()
next(isolation_context) # Enter.
assert mock_evm.take_snapshot.call_count == 1
assert mock_evm.revert_to_snapshot.call_count == 0
next(isolation_context, None) # Exit.
# It doesn't even try!
assert mock_evm.revert_to_snapshot.call_count == 0

def test_isolation_restore_fails_avoids_snapshot_next_time(
self, networks, use_mock_provider, fixtures, mock_evm
):
mock_evm.take_snapshot.return_value = 123
mock_evm.revert_to_snapshot.side_effect = NotImplementedError
isolation_context = fixtures._isolation()
next(isolation_context) # Enter.
# Snapshot works, we get this far.
assert mock_evm.take_snapshot.call_count == 1
assert mock_evm.revert_to_snapshot.call_count == 0

# At this point, it is realized snapshotting is no-go.
mock_evm.take_snapshot.reset_mock()
next(isolation_context, None) # Exit.
isolation_context = fixtures._isolation()
next(isolation_context) # Enter again.
# This time, snapshotting is NOT attempted.
assert mock_evm.take_snapshot.call_count == 0

def test_isolation_supports_flag_set_after_successful_snapshot(
self, networks, use_mock_provider, fixtures, mock_evm
):
"""
Testing the unusual case where `._supports_snapshot` was changed manually after
a successful snapshot and before the restore attempt.
"""
mock_evm.take_snapshot.return_value = 123
isolation_context = fixtures._isolation()
next(isolation_context) # Enter.
assert mock_evm.take_snapshot.call_count == 1
assert mock_evm.revert_to_snapshot.call_count == 0

# HACK: Change the flag manually to show it will avoid
# the restore.
fixtures._supports_snapshot = False

next(isolation_context, None) # Exit.
# Even though snapshotting worked, the flag was changed,
# and so the restore never gets attempted.
assert mock_evm.revert_to_snapshot.call_count == 0
def test_insert_isolation_fixtures(mocker):
mock_item = mocker.MagicMock()

def _create_fixture_entry(name: str, scope: str):
mock_fixture = mocker.MagicMock()
mock_fixture.scope = scope
mock_fixture.name = name
return mock_fixture

fixtures = {
"fixture_at_function": [_create_fixture_entry("fixture_at_function", "function")],
"fixture_at_session": [_create_fixture_entry("fixture_at_session", "session")],
"fixture_at_module": [_create_fixture_entry("fixture_at_module", "module")],
"other_random_fixture": [_create_fixture_entry("other_random_fixture", "function")],
}

mock_item.session._fixturemanager._arg2fixturedefs = fixtures
mock_item.fixturenames = [*list(fixtures.keys()), "otheriteminnames"]
_insert_isolation_fixtures(mock_item)
actual = mock_item.fixturenames
expected = [
"_function_isolation",
"_module_isolation",
"_session_isolation",
"fixture_at_function",
"fixture_at_session",
"fixture_at_module",
"other_random_fixture",
"otheriteminnames",
]
assert actual == expected

0 comments on commit c590099

Please sign in to comment.