import logging
import sys
import unittest
from contextlib import contextmanager
from nose2 import util
from nose2._vendor import six
from nose2.main import PluggableTestProgram
log = logging.getLogger(__name__)
__unittest = True
LAYERS_PLUGIN_NOT_LOADED_MESSAGE = (
"Warning: Such will not function properly if "
'the "nose2.plugins.layers" plugin not loaded!\n'
)
[docs]@contextmanager
def A(description):
"""Test scenario context manager.
Returns a :class:`nose2.tools.such.Scenario` instance,
which by convention is bound to ``it``:
.. code-block :: python
with such.A('test scenario') as it:
# tests and fixtures
"""
yield Scenario(description)
class Helper(unittest.TestCase):
def runTest(self):
pass
helper = Helper()
[docs]class Scenario(object):
"""A test scenario.
A test scenario defines a set of fixtures and tests
that depend on those fixtures.
"""
_helper = helper
def __init__(self, description):
self._group = Group("A %s" % description, 0)
[docs] @contextmanager
def having(self, description):
"""Define a new group under the current group.
Fixtures and tests defined within the block will
belong to the new group.
.. code-block :: python
with it.having('a description of this group'):
# ...
"""
last = self._group
self._group = self._group.child("having %s" % description)
log.debug("starting new group from %s", description)
yield self
log.debug("leaving group %s", description)
self._group = last
def uses(self, layer):
log.debug("Adding %s as mixin to %s", layer, self._group)
self._group.mixins.append(layer)
[docs] def has_setup(self, func):
"""Add a :func:`setup` method to this group.
The :func:`setup` method will run once, before any of the
tests in the containing group.
A group may define any number of :func:`setup` functions. They
will execute in the order in which they are defined.
.. code-block :: python
@it.has_setup
def setup():
# ...
"""
self._group.addSetup(func)
return func
[docs] def has_teardown(self, func):
"""Add a :func:`teardown` method to this group.
The :func:`teardown` method will run once, after all of the
tests in the containing group.
A group may define any number of :func:`teardown` functions. They
will execute in the order in which they are defined.
.. code-block :: python
@it.has_teardown
def teardown():
# ...
"""
self._group.addTeardown(func)
return func
[docs] def has_test_setup(self, func):
"""Add a test case :func:`setup` method to this group.
The :func:`setup` method will run before each of the
tests in the containing group.
A group may define any number of test case :func:`setup`
functions. They will execute in the order in which they are
defined.
Test :func:`setup` functions may optionally take one argument. If
they do, they will be passed the :class:`unittest.TestCase`
instance generated for the test.
.. code-block :: python
@it.has_test_setup
def setup(case):
# ...
"""
self._group.addTestSetUp(func)
[docs] def has_test_teardown(self, func):
"""Add a test case :func:`teardown` method to this group.
The :func:`teardown` method will run before each of the
tests in the containing group.
A group may define any number of test case :func:`teardown`
functions. They will execute in the order in which they are
defined.
Test :func:`teardown` functions may optionally take one argument. If
they do, they will be passed the :class:`unittest.TestCase`
instance generated for the test.
.. code-block :: python
@it.has_test_teardown
def teardown(case):
# ...
"""
self._group.addTestTearDown(func)
[docs] def should(self, desc):
"""Define a test case.
Each function marked with this decorator becomes a test
case in the current group.
The decorator takes one optional argument, the description
of the test case: what it **should** do. If this argument
is not provided, the docstring of the decorated function
will be used as the test case description.
Test functions may optionally take one argument. If they do,
they will be passed the :class:`unittest.TestCase` instance generated
for the test. They can use this TestCase instance to execute assert
methods, among other things.
.. code-block :: python
@it.should('do this')
def dothis(case):
# ....
@it.should
def dothat():
"do that also"
# ....
"""
def decorator(f):
_desc = desc if isinstance(desc, six.string_types) else f.__doc__
case = Case(self._group, f, "should %s" % _desc)
self._group.addCase(case)
return case
if isinstance(desc, type(decorator)):
return decorator(desc)
return decorator
def __getattr__(self, attr):
return getattr(self._helper, attr)
[docs] def createTests(self, mod):
"""Generate test cases for this scenario.
.. warning ::
You must call this, passing in :func:`globals`, to
generate tests from the scenario. If you don't,
**no tests will be created**.
.. code-block :: python
it.createTests(globals())
"""
self._checkForLayersPlugin()
self._makeGroupTest(mod, self._group)
def _checkForLayersPlugin(self):
currentSession = PluggableTestProgram.getCurrentSession()
if not currentSession:
return
if not currentSession.isPluginLoaded("nose2.plugins.layers"):
sys.stderr.write(LAYERS_PLUGIN_NOT_LOADED_MESSAGE)
def _makeGroupTest(self, mod, group, parent_layer=None, position=0):
layer = self._makeLayer(group, parent_layer, position)
case = self._makeTestCase(group, layer, parent_layer)
log.debug("Made test case %s with layer %s from %s", case, layer, group)
mod[layer.__name__] = layer
layer.__module__ = mod["__name__"]
name = case.__name__
long_name = " ".join([n[0].description for n in util.ancestry(layer)] + [name])
mod[long_name] = case
if name not in mod:
mod[name] = case
case.__module__ = mod["__name__"]
for index, child in enumerate(group._children):
self._makeGroupTest(mod, child, layer, index)
def _makeTestCase(self, group, layer, parent_layer):
attr = {"layer": layer, "group": group, "description": group.description}
def _make_test_func(case):
"""
Needs to be outside of the for-loop scope, so that ``case`` is properly
registered as a closure.
"""
def _test(s, *args):
case(s, *args)
return _test
for index, case in enumerate(group._cases):
name = "test %04d: %s" % (index, case.description)
_test = _make_test_func(case)
_test.__name__ = name
_test.description = case.description
_test.case = case
_test.index = index
if hasattr(case.func, "paramList"):
_test.paramList = case.func.paramList
attr[name] = _test # for collection and sorting
attr[case.description] = _test # for random access by name
setups = getattr(parent_layer, "testSetups", []) + group._test_setups
if setups:
def setUp(self):
for func in setups:
util.call_with_args_if_expected(func, self)
attr["setUp"] = setUp
teardowns = (
getattr(parent_layer, "testTeardowns", []) + group._test_teardowns[:]
)
if teardowns:
def tearDown(self):
for func in teardowns:
util.call_with_args_if_expected(func, self)
attr["tearDown"] = tearDown
def methodDescription(self):
return getattr(self, self._testMethodName).description
attr["methodDescription"] = methodDescription
return type(group.description, (unittest.TestCase,), attr)
def _makeLayer(self, group, parent_layer=None, position=0):
if parent_layer is None:
parent_layer = object
def setUp(cls):
for func in cls.setups:
util.call_with_args_if_expected(func, self)
def tearDown(cls):
for func in cls.teardowns:
util.call_with_args_if_expected(func, self)
attr = {
"description": group.description,
"setUp": classmethod(setUp),
"tearDown": classmethod(tearDown),
"setups": group._setups[:],
"testSetups": getattr(parent_layer, "testSetups", []) + group._test_setups,
"teardowns": group._teardowns[:],
"testTeardowns": getattr(parent_layer, "testTeardowns", [])
+ group._test_teardowns[:],
"position": position,
"mixins": (),
}
if group.base_layer:
# inject this layer into the group class list
# by making it a subclass of parent_layer
layer = group.base_layer
if parent_layer not in layer.__bases__:
layer.mixins = (parent_layer,)
else:
layer = type("%s:layer" % group.description, (parent_layer,), attr)
if group.mixins:
layer.mixins = getattr(layer, "mixins", ()) + tuple(group.mixins)
log.debug(
"made layer %s with bases %s and mixins %s",
layer,
layer.__bases__,
layer.mixins,
)
return layer
class Group(object):
"""A group of tests, with common fixtures and description"""
def __init__(self, description, indent=0, parent=None, base_layer=None):
self.description = description
self.indent = indent
self.parent = parent
self.base_layer = base_layer
self.mixins = []
self._cases = []
self._setups = []
self._teardowns = []
self._test_setups = []
self._test_teardowns = []
self._children = []
def addCase(self, case):
if not self._cases:
case.first = True
case.indent = self.indent
self._cases.append(case)
def addSetup(self, func):
self._setups.append(func)
def addTeardown(self, func):
self._teardowns.append(func)
def addTestSetUp(self, func):
self._test_setups.append(func)
def addTestTearDown(self, func):
self._test_teardowns.append(func)
def fullDescription(self):
d = []
p = self.parent
while p:
d.insert(0, p.description)
p = p.parent
d.append(self.description)
return " ".join(d)
def child(self, description, base_layer=None):
child = Group(description, self.indent + 1, self, base_layer)
self._children.append(child)
return child
class Case(object):
"""Information about a test case"""
_helper = helper
def __init__(self, group, func, description):
self.group = group
self.func = func
self.description = description
self._setups = []
self._teardowns = []
self.first = False
self.full = False
def __call__(self, testcase, *args):
# ... only if it takes an arg
self._helper = testcase
util.call_with_args_if_expected(self.func, testcase, *args)
def __getattr__(self, attr):
return getattr(self._helper, attr)