From 43ed6aa4c158ed64edbc10a67191a792494053ec Mon Sep 17 00:00:00 2001 From: Lars Holm Nielsen Date: Wed, 9 Sep 2020 18:10:23 +0200 Subject: [PATCH] fixtures: new entry_points fixture * Adds a new entry_points fixture which can inject extra entry points during application loading to avoid having to manually registering features. --- pytest.ini | 1 + pytest_invenio/__init__.py | 27 +++++++++++++ pytest_invenio/fixtures.py | 81 ++++++++++++++++++++++++++++++++++++++ pytest_invenio/plugin.py | 5 ++- run-tests.sh | 2 +- tests/test_fixtures.py | 54 +++++++++++++++++++++++++ 6 files changed, 167 insertions(+), 3 deletions(-) diff --git a/pytest.ini b/pytest.ini index 3e5122a..b3e30bc 100644 --- a/pytest.ini +++ b/pytest.ini @@ -10,3 +10,4 @@ pep8ignore = docs/conf.py ALL addopts = --pep8 --doctest-glob="*.rst" --doctest-modules --cov=pytest_invenio --cov-report=term-missing --cov-append testpaths = docs tests pytest_invenio +filterwarnings = ignore::pytest.PytestDeprecationWarning diff --git a/pytest_invenio/__init__.py b/pytest_invenio/__init__.py index 8dd63a2..30c31f9 100644 --- a/pytest_invenio/__init__.py +++ b/pytest_invenio/__init__.py @@ -240,6 +240,33 @@ def create_app(): instance you may want to customize the celery configuration only for a specific Python test file. +Injecting entry points +~~~~~~~~~~~~~~~~~~~~~~ +Invenio relies heavily upon entry points for constructing a Flask application, +and it can be rather cumbersome to try to manually register database models, +mappings and other features afterwards. + +You can therefore inject extra entry points if needed during testing via the +:py:data:`~fixtures.extra_entry_points` fixture and using it in your custom +``create_app()`` fixture: + +.. code-block:: python + + @pytest.fixture(scope="module") + def extra_entry_points(): + return { + 'invenio_db.models': [ + 'mock_module = mock_module.models', + ] + } + + @pytest.fixture(scope="module") + def create_app(entry_points): + return _create_api + +Note that ``create_app()`` depends on the :py:data:`~fixtures.entry_points` +fixture not the ``extra_entry_points()``. + .. _views-testing: Views testing diff --git a/pytest_invenio/fixtures.py b/pytest_invenio/fixtures.py index eae39e3..8714582 100644 --- a/pytest_invenio/fixtures.py +++ b/pytest_invenio/fixtures.py @@ -17,6 +17,7 @@ import sys import tempfile from datetime import datetime +from functools import partial import pkg_resources import pytest @@ -633,3 +634,83 @@ def create_bucket_from_dir(source_dir, location_obj=None): db.session.commit() return bucket_obj return create_bucket_from_dir + + +class MockDistribution(pkg_resources.Distribution): + """A mocked distribution that we can inject entry points with.""" + + def __init__(self, extra_entry_points): + """Initialise the extra entry point.""" + self._ep_map = {} + # Create the entry point group map (which eventually will be used to + # iterate over entry points). See source code for Distribution, + # EntryPoint and WorkingSet in pkg_resources module. + for group, entries in extra_entry_points.items(): + group_map = {} + for ep_str in entries: + ep = pkg_resources.EntryPoint.parse(ep_str) + ep.require = self._require_noop + group_map[ep.name] = ep + self._ep_map[group] = group_map + # Note location must have a non-empty string value, as it is used as a + # key into a dictionary. + super().__init__(location='unknown') + + def _require_noop(self, *args, **kwargs): + """Do nothing on entry point require.""" + pass + + +@pytest.fixture(scope="module") +def entry_points(extra_entry_points): + """Entry points fixture. + + Scope: module + + Invenio relies heavily on Python entry points for constructing an + application and it can be rather cumbersome to try to register database + models, search mappings etc yourself afterwards. + + This fixture allows you to inject extra entry points into the application + loading, so that you can load e.g. a testing module or test mapping. + + To use the fixture simply define the ``extra_entry_points()`` fixture, + and then depend on the ``entry_points()`` fixture in your ``create_app`` + fixture: + + .. code-block:: python + + @pytest.fixture(scope="module") + def extra_entry_points(): + return { + 'invenio_db.models': [ + 'mock_module = mock_module.models', + ] + } + + @pytest.fixture(scope="module") + def create_app(instance_path, entry_points): + return _create_api + """ + # First make a copy of the working_set state, so that we can restore the + # state. + workingset_state = pkg_resources.working_set.__getstate__() + + # Next, make a fake distribution that wil yield the extra entry points and + # add it to the global working_set. + dist = MockDistribution(extra_entry_points) + pkg_resources.working_set.add(dist) + + yield dist + + # Last, we restore the original workingset state. + pkg_resources.working_set.__setstate__(workingset_state) + + +@pytest.fixture(scope="module") +def extra_entry_points(): + """Extra entry points. + + Overwrite this fixture to define extra entry points. + """ + return {} diff --git a/pytest_invenio/plugin.py b/pytest_invenio/plugin.py index e1c90ce..e9a6d7a 100644 --- a/pytest_invenio/plugin.py +++ b/pytest_invenio/plugin.py @@ -26,8 +26,9 @@ from .fixtures import _monkeypatch_response_class, app, app_config, appctx, \ base_app, base_client, broker_uri, browser, bucket_from_dir, \ - celery_config_ext, cli_runner, database, db, db_uri, default_handler, es, \ - es_clear, instance_path, location, mailbox, script_info + celery_config_ext, cli_runner, database, db, db_uri, default_handler, \ + entry_points, es, es_clear, extra_entry_points, instance_path, location, \ + mailbox, script_info def pytest_generate_tests(metafunc): diff --git a/run-tests.sh b/run-tests.sh index 448ac4b..998e420 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -12,7 +12,7 @@ rm -f .coverage rm -f .coverage.eager.* pydocstyle pytest_invenio tests docs && \ -isort -rc -c -df && \ +isort pytest_invenio tests --check-only --diff && \ check-manifest --ignore ".travis-*" && \ sphinx-build -qnNW docs docs/_build/html && \ # Following is needed in order to get proper code coverage for pytest plugins. diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 5d8e266..4b47f9d 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -431,3 +431,57 @@ def test_creating_location_and_use_bucket_from_dir(bucket_from_dir): assert files_from_bucket.one().key == "output_file" """) conftest_testdir.runpytest().assert_outcomes(passed=1) + + +def test_entrypoint(testdir): + """Test database creation and initialization.""" + testdir.makeconftest(""" + import pytest + + from flask import Flask + from functools import partial + from invenio_db import InvenioDB + + def _factory(name, **config): + app_ = Flask(name) + app_.config.update(**config) + InvenioDB(app_) + return app_ + + @pytest.fixture(scope='module') + def create_app(entry_points): + return partial(_factory, 'app') + """) + testdir.makepyfile(mock_module=""" + from invenio_db import db + from pkg_resources import iter_entry_points + + class Place(db.Model): + id = db.Column(db.Integer, primary_key=True) + + def db_entry_points(): + return list(iter_entry_points('invenio_db.models')) + """) + testdir.makepyfile(test_ep=""" + import pytest + # By importing we get a reference to iter_entry_points before it + # has been mocked (to test that this case also works). + from mock_module import db_entry_points + + + @pytest.fixture(scope='module') + def extra_entry_points(): + return { + 'invenio_db.models': [ + 'mock_module = mock_module', + ], + } + + def test_app(base_app, db): + from mock_module import Place + assert Place.query.count() == 0 + + def test_extras(base_app, db): + assert len(db_entry_points()) == 3 + """) + testdir.runpytest("-s").assert_outcomes(passed=2)