# -*- coding: utf8 -*-
########################################################################################
# This file is part of exhale. Copyright (c) 2017-2019, Stephen McDowell. #
# Full BSD 3-Clause license available here: #
# #
# https://github.com/svenevs/exhale/blob/master/LICENSE #
########################################################################################
"""
The decorators module defines useful class / function decorators for test cases.
"""
from __future__ import unicode_literals
from copy import deepcopy
from inspect import isclass
import pytest
from .utils import deep_update
__all__ = ["default_confoverrides", "confoverrides", "no_run"]
[docs]def _apply_confoverride_to_class(cls, config, priority):
"""
Apply a configuration override ``config`` to class ``cls`` with a given priority.
We need the priority trick as the default configuration is applied before a possible
class-wide decorator, which should supersede the default configuration. We use
``pytest.mark.exhale`` as a store of ``kwargs`` to apply to ``pytest.mark.sphinx``,
and we use the priority to combine these kwargs with respect to priorities.
This method performs the ultimate ``pytest.mark.sphinx``.
**Parameters**
``cls`` (Subclass of :class:`testing.base.ExhaleTestCase`)
The class to apply the ``confoverride`` to.
``config`` (:class:`python:dict`)
The dictionary of ``confoverrides`` as would be supplied to
``pytest.mark.sphinx``. These are the overrides to ``conf.py``.
``priority`` (:class:`python:int`)
The positive integer indicating the priority of the new ``config`` updates,
higher values take precedence over lower values.
For example, when :func:`testing.decorators.default_confoverrides` calls
this function, the priority is ``1``. When the decorator
:func:`testing.decorators.confoverrides` is applied to a class, the priority
is ``2``. Finally, when :func:`testing.decorators.confoverrides` is applied
to a test function, its priority is ``3``. Thus the function-level override
has the highest priority, meaning any conflicting values with lower level
priorities will lose out to the function-level override.
**Return**
Subclass of :class:`testing.base.ExhaleTestCase`
The input ``cls`` is returned.
"""
# look for test methods in the class
for name, meth in cls.__dict__.items():
if not callable(meth) or not name.startswith("test_"):
continue
# meth is a test method, let's go and mark it!
# create marker kwargs
kwargs = dict(confoverrides=deepcopy(config))
# apply the exhale marker to the method, so that priority is saved
meth = pytest.mark.exhale(priority, **kwargs)(meth)
# create a list of exhale markers kwargs
markers_kwargs = []
if getattr(meth, "pytestmark", False):
for mark in meth.pytestmark:
if mark.name == "exhale":
# mark.args[0]: the priority from pytest.mark.exhale
# mark.kwargs: the kwargs to override with said priority
markers_kwargs.append((mark.args[0], mark.kwargs))
# sort that list according to priority
markers_kwargs.sort(key=lambda m: m[0])
# now we can generate the sphinx fixture kwargs by combining the above list of
# kwargs depending on priority
sphinx_kwargs = {}
for __, kw in markers_kwargs:
deep_update(sphinx_kwargs, kw)
# and finally we set the sphinx markers with the combined kwargs, that override
# the previous ones
setattr(cls, name, pytest.mark.sphinx(**sphinx_kwargs)(meth))
return cls
[docs]def default_confoverrides(cls, config):
"""
Apply the default configuration config to test class ``cls``.
This configuration is set with a priority of ``1`` so that it may be overridden by
the :func:`testing.decorators.confoverrides` decorator applied to test classes and
functions.
**Parameters**
``cls`` (Subclass of :class:`testing.base.ExhaleTestCase`)
The class to apply the ``config`` dictionary as ``confoverrides`` to.
``config`` (:class:`python:dict`)
The dictionary of ``confoverrides`` as would be supplied to
``pytest.mark.sphinx``. These are the overrides to ``conf.py``.
**Return**
Subclass of :class:`testing.base.ExhaleTestCase`
The input ``cls`` is returned.
"""
return _apply_confoverride_to_class(cls, config, 1)
[docs]def confoverrides(**config):
"""
Override the defaults of |make_default_config| to supply to ``pytest.mark.sphinx``.
.. |make_default_config| replace:: :func:`testing.base.make_default_config`
It can be applied to a test method or a test class, which is equivalent to
decorating every method in that class.
Usage:
.. code-block:: py
@confoverrides(var1=value1, var2=value2)
def test_something(self):
...
or:
.. code-block:: py
@confoverrides(var1=value1, var2=value2)
class MyTestCase(ExhaleTestCase):
test_project = 'my_project'
...
Typical usage of this decorator is to modify a value in ``exhale_args``, such as
.. code-block:: py
@confoverrides(exhale_args={"containmentFolder": "./alt_api"})
def test_alt_out(self):
...
However, this decorator can be used to change or set any value you would typically
find in a ``conf.py`` file.
**Parameters**
``**config`` (:class:`python:dict`)
The dictionary of ``confoverrides`` as would be supplied to
``pytest.mark.sphinx``. These are the overrides to ``conf.py``.
**Return**
(``class`` or :data:`python:types.FunctionType`)
The decorated class or function.
"""
def actual_decorator(meth_or_cls):
if not config:
return meth_or_cls
if isclass(meth_or_cls):
return _apply_confoverride_to_class(meth_or_cls, config, 2)
else:
return pytest.mark.exhale(3, confoverrides=config)(meth_or_cls)
return actual_decorator
[docs]def no_cleanup(method):
"""
Prevent the "docs" directory and generated Doxygen / API from being deleted.
Usage:
.. code-block:: py
class CMathsTests(ExhaleTestCase):
# docs dir, generated API, and Doxygen will not be deleted so that you can
# inspect what may be causing your test to fail
@no_cleanup
def test_being_developed(self):
pass
.. danger::
This decorator performs ``self.testroot = [self.testroot]`` as an internal bypass
to the fixtures created in ``__new__`` for the metaclass. Specifically, the
fixtures generated check ``if isinstance(self.testroot, six.string_types)``.
As such, since ``self.testroot`` may be desired in the given ``@no_cleanup``
function, you must acquire it with ``testroot = self.testroot[0]``. This is a
hacky solution, but should be sufficient. You have been warned.
**Parameters**
``method`` (:data:`python:types.FunctionType`)
**Must** be an instance-level testing function of a derived type of
:class:`testing.base.ExhaleTestCase`. The function should have only a
single parameter ``self``.
**Return**
(:data:`python:types.FunctionType`)
The decorated function, which simply calls the provided function and sets
``self.testroot = None``. Click on ``[source]`` link for
:func:`ExhaleTestCaseMetaclass.__new__ <testing.base.ExhaleTestCaseMetaclass.__new__>`
and search for ``@no_cleanup`` to see how this prevents cleanup.
"""
def actual_no_cleanup(self):
self.testroot = [self.testroot]
method(self)
return actual_no_cleanup
[docs]def no_run(obj):
"""
Disable the generation of ``*.rst`` files in a specific test method.
It can be applied to a test class, which will be equivalent to decorating every
method in that class.
Usage:
.. code-block:: py
@no_run
def test_something(self):
...
or:
.. code-block:: py
@no_run
class MyTestCase(ExhaleTestCase):
test_project = 'my_project'
...
Internally this will use the :func:`testing.fixtures.no_run` fixture.
**Parameters**
``obj`` (``class`` or :data:`python:types.FunctionType`)
The class or function to disable exhale from generating reStructuredText
documents for.
**Return**
``class`` or :data:`python:types.FunctionType`
The decorated ``obj``.
"""
return pytest.mark.usefixtures("no_run")(obj)