Skip to content

Commit

Permalink
fixtures: new entry_points fixture
Browse files Browse the repository at this point in the history
* Adds a new entry_points fixture which can inject extra entry points
  during application loading to avoid having to manually registering
  features.
  • Loading branch information
lnielsen committed Sep 13, 2020
1 parent 9fc014c commit 85f5acf
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 3 deletions.
1 change: 1 addition & 0 deletions pytest.ini
Expand Up @@ -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
27 changes: 27 additions & 0 deletions pytest_invenio/__init__.py
Expand Up @@ -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
Expand Down
81 changes: 81 additions & 0 deletions pytest_invenio/fixtures.py
Expand Up @@ -17,6 +17,7 @@
import sys
import tempfile
from datetime import datetime
from functools import partial

import pkg_resources
import pytest
Expand Down Expand Up @@ -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 {}
5 changes: 3 additions & 2 deletions pytest_invenio/plugin.py
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion run-tests.sh
Expand Up @@ -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.
Expand Down
57 changes: 57 additions & 0 deletions tests/test_fixtures.py
Expand Up @@ -431,3 +431,60 @@ 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 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):
for ep in db_entry_points():
if ep.name == 'mock_module':
return
assert False, "mock_module not found in entry points"
""")
testdir.runpytest("-s").assert_outcomes(passed=2)

0 comments on commit 85f5acf

Please sign in to comment.