# Source code for nose2.tools.such

    import logging
    import sys
    import unittest
    from contextlib import contextmanager

    from nose2 import util
    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'
    )

    @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()

    class Scenario:
        """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)

        @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)

        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

        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

        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)

        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)

        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, str) 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)

        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:
        """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:
        """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)