diff --git a/src/ape/pytest/runners.py b/src/ape/pytest/runners.py index a0a8c64c84..303ccf0451 100644 --- a/src/ape/pytest/runners.py +++ b/src/ape/pytest/runners.py @@ -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): """ @@ -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") diff --git a/tests/functional/test_fixtures.py b/tests/functional/test_fixtures.py index b27b3979f7..9fa4ef644f 100644 --- a/tests/functional/test_fixtures.py +++ b/tests/functional/test_fixtures.py @@ -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 diff --git a/tests/functional/test_test.py b/tests/functional/test_test.py index 28151b70d0..62c30142e5 100644 --- a/tests/functional/test_test.py +++ b/tests/functional/test_test.py @@ -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 @@ -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