1# -*- coding: utf-8 -*-
2from __future__ import absolute_import
3from __future__ import division
4from __future__ import print_function
5
6import os
7import warnings
8
9import py
10import six
11
12import _pytest._code
13from _pytest.compat import getfslineno
14from _pytest.mark.structures import NodeKeywords
15from _pytest.outcomes import fail
16
17SEP = "/"
18
19tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
20
21
22def _splitnode(nodeid):
23    """Split a nodeid into constituent 'parts'.
24
25    Node IDs are strings, and can be things like:
26        ''
27        'testing/code'
28        'testing/code/test_excinfo.py'
29        'testing/code/test_excinfo.py::TestFormattedExcinfo'
30
31    Return values are lists e.g.
32        []
33        ['testing', 'code']
34        ['testing', 'code', 'test_excinfo.py']
35        ['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo', '()']
36    """
37    if nodeid == "":
38        # If there is no root node at all, return an empty list so the caller's logic can remain sane
39        return []
40    parts = nodeid.split(SEP)
41    # Replace single last element 'test_foo.py::Bar' with multiple elements 'test_foo.py', 'Bar'
42    parts[-1:] = parts[-1].split("::")
43    return parts
44
45
46def ischildnode(baseid, nodeid):
47    """Return True if the nodeid is a child node of the baseid.
48
49    E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz', but not of 'foo/blorp'
50    """
51    base_parts = _splitnode(baseid)
52    node_parts = _splitnode(nodeid)
53    if len(node_parts) < len(base_parts):
54        return False
55    return node_parts[: len(base_parts)] == base_parts
56
57
58class Node(object):
59    """ base class for Collector and Item the test collection tree.
60    Collector subclasses have children, Items are terminal nodes."""
61
62    def __init__(
63        self, name, parent=None, config=None, session=None, fspath=None, nodeid=None
64    ):
65        #: a unique name within the scope of the parent node
66        self.name = name
67
68        #: the parent collector node.
69        self.parent = parent
70
71        #: the pytest config object
72        self.config = config or parent.config
73
74        #: the session this node is part of
75        self.session = session or parent.session
76
77        #: filesystem path where this node was collected from (can be None)
78        self.fspath = fspath or getattr(parent, "fspath", None)
79
80        #: keywords/markers collected from all scopes
81        self.keywords = NodeKeywords(self)
82
83        #: the marker objects belonging to this node
84        self.own_markers = []
85
86        #: allow adding of extra keywords to use for matching
87        self.extra_keyword_matches = set()
88
89        # used for storing artificial fixturedefs for direct parametrization
90        self._name2pseudofixturedef = {}
91
92        if nodeid is not None:
93            assert "::()" not in nodeid
94            self._nodeid = nodeid
95        else:
96            self._nodeid = self.parent.nodeid
97            if self.name != "()":
98                self._nodeid += "::" + self.name
99
100    @property
101    def ihook(self):
102        """ fspath sensitive hook proxy used to call pytest hooks"""
103        return self.session.gethookproxy(self.fspath)
104
105    def __repr__(self):
106        return "<%s %s>" % (self.__class__.__name__, getattr(self, "name", None))
107
108    def warn(self, warning):
109        """Issue a warning for this item.
110
111        Warnings will be displayed after the test session, unless explicitly suppressed
112
113        :param Warning warning: the warning instance to issue. Must be a subclass of PytestWarning.
114
115        :raise ValueError: if ``warning`` instance is not a subclass of PytestWarning.
116
117        Example usage:
118
119        .. code-block:: python
120
121            node.warn(PytestWarning("some message"))
122
123        """
124        from _pytest.warning_types import PytestWarning
125
126        if not isinstance(warning, PytestWarning):
127            raise ValueError(
128                "warning must be an instance of PytestWarning or subclass, got {!r}".format(
129                    warning
130                )
131            )
132        path, lineno = get_fslocation_from_item(self)
133        warnings.warn_explicit(
134            warning,
135            category=None,
136            filename=str(path),
137            lineno=lineno + 1 if lineno is not None else None,
138        )
139
140    # methods for ordering nodes
141    @property
142    def nodeid(self):
143        """ a ::-separated string denoting its collection tree address. """
144        return self._nodeid
145
146    def __hash__(self):
147        return hash(self.nodeid)
148
149    def setup(self):
150        pass
151
152    def teardown(self):
153        pass
154
155    def listchain(self):
156        """ return list of all parent collectors up to self,
157            starting from root of collection tree. """
158        chain = []
159        item = self
160        while item is not None:
161            chain.append(item)
162            item = item.parent
163        chain.reverse()
164        return chain
165
166    def add_marker(self, marker, append=True):
167        """dynamically add a marker object to the node.
168
169        :type marker: ``str`` or ``pytest.mark.*``  object
170        :param marker:
171            ``append=True`` whether to append the marker,
172            if ``False`` insert at position ``0``.
173        """
174        from _pytest.mark import MarkDecorator, MARK_GEN
175
176        if isinstance(marker, six.string_types):
177            marker = getattr(MARK_GEN, marker)
178        elif not isinstance(marker, MarkDecorator):
179            raise ValueError("is not a string or pytest.mark.* Marker")
180        self.keywords[marker.name] = marker
181        if append:
182            self.own_markers.append(marker.mark)
183        else:
184            self.own_markers.insert(0, marker.mark)
185
186    def iter_markers(self, name=None):
187        """
188        :param name: if given, filter the results by the name attribute
189
190        iterate over all markers of the node
191        """
192        return (x[1] for x in self.iter_markers_with_node(name=name))
193
194    def iter_markers_with_node(self, name=None):
195        """
196        :param name: if given, filter the results by the name attribute
197
198        iterate over all markers of the node
199        returns sequence of tuples (node, mark)
200        """
201        for node in reversed(self.listchain()):
202            for mark in node.own_markers:
203                if name is None or getattr(mark, "name", None) == name:
204                    yield node, mark
205
206    def get_closest_marker(self, name, default=None):
207        """return the first marker matching the name, from closest (for example function) to farther level (for example
208        module level).
209
210        :param default: fallback return value of no marker was found
211        :param name: name to filter by
212        """
213        return next(self.iter_markers(name=name), default)
214
215    def listextrakeywords(self):
216        """ Return a set of all extra keywords in self and any parents."""
217        extra_keywords = set()
218        for item in self.listchain():
219            extra_keywords.update(item.extra_keyword_matches)
220        return extra_keywords
221
222    def listnames(self):
223        return [x.name for x in self.listchain()]
224
225    def addfinalizer(self, fin):
226        """ register a function to be called when this node is finalized.
227
228        This method can only be called when this node is active
229        in a setup chain, for example during self.setup().
230        """
231        self.session._setupstate.addfinalizer(fin, self)
232
233    def getparent(self, cls):
234        """ get the next parent node (including ourself)
235        which is an instance of the given class"""
236        current = self
237        while current and not isinstance(current, cls):
238            current = current.parent
239        return current
240
241    def _prunetraceback(self, excinfo):
242        pass
243
244    def _repr_failure_py(self, excinfo, style=None):
245        if excinfo.errisinstance(fail.Exception):
246            if not excinfo.value.pytrace:
247                return six.text_type(excinfo.value)
248        fm = self.session._fixturemanager
249        if excinfo.errisinstance(fm.FixtureLookupError):
250            return excinfo.value.formatrepr()
251        tbfilter = True
252        if self.config.getoption("fulltrace", False):
253            style = "long"
254        else:
255            tb = _pytest._code.Traceback([excinfo.traceback[-1]])
256            self._prunetraceback(excinfo)
257            if len(excinfo.traceback) == 0:
258                excinfo.traceback = tb
259            tbfilter = False  # prunetraceback already does it
260            if style == "auto":
261                style = "long"
262        # XXX should excinfo.getrepr record all data and toterminal() process it?
263        if style is None:
264            if self.config.getoption("tbstyle", "auto") == "short":
265                style = "short"
266            else:
267                style = "long"
268
269        if self.config.getoption("verbose", 0) > 1:
270            truncate_locals = False
271        else:
272            truncate_locals = True
273
274        try:
275            os.getcwd()
276            abspath = False
277        except OSError:
278            abspath = True
279
280        return excinfo.getrepr(
281            funcargs=True,
282            abspath=abspath,
283            showlocals=self.config.getoption("showlocals", False),
284            style=style,
285            tbfilter=tbfilter,
286            truncate_locals=truncate_locals,
287        )
288
289    repr_failure = _repr_failure_py
290
291
292def get_fslocation_from_item(item):
293    """Tries to extract the actual location from an item, depending on available attributes:
294
295    * "fslocation": a pair (path, lineno)
296    * "obj": a Python object that the item wraps.
297    * "fspath": just a path
298
299    :rtype: a tuple of (str|LocalPath, int) with filename and line number.
300    """
301    result = getattr(item, "location", None)
302    if result is not None:
303        return result[:2]
304    obj = getattr(item, "obj", None)
305    if obj is not None:
306        return getfslineno(obj)
307    return getattr(item, "fspath", "unknown location"), -1
308
309
310class Collector(Node):
311    """ Collector instances create children through collect()
312        and thus iteratively build a tree.
313    """
314
315    class CollectError(Exception):
316        """ an error during collection, contains a custom message. """
317
318    def collect(self):
319        """ returns a list of children (items and collectors)
320            for this collection node.
321        """
322        raise NotImplementedError("abstract")
323
324    def repr_failure(self, excinfo):
325        """ represent a collection failure. """
326        if excinfo.errisinstance(self.CollectError):
327            exc = excinfo.value
328            return str(exc.args[0])
329
330        # Respect explicit tbstyle option, but default to "short"
331        # (None._repr_failure_py defaults to "long" without "fulltrace" option).
332        tbstyle = self.config.getoption("tbstyle", "auto")
333        if tbstyle == "auto":
334            tbstyle = "short"
335
336        return self._repr_failure_py(excinfo, style=tbstyle)
337
338    def _prunetraceback(self, excinfo):
339        if hasattr(self, "fspath"):
340            traceback = excinfo.traceback
341            ntraceback = traceback.cut(path=self.fspath)
342            if ntraceback == traceback:
343                ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
344            excinfo.traceback = ntraceback.filter()
345
346
347def _check_initialpaths_for_relpath(session, fspath):
348    for initial_path in session._initialpaths:
349        if fspath.common(initial_path) == initial_path:
350            return fspath.relto(initial_path)
351
352
353class FSCollector(Collector):
354    def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None):
355        fspath = py.path.local(fspath)  # xxx only for test_resultlog.py?
356        name = fspath.basename
357        if parent is not None:
358            rel = fspath.relto(parent.fspath)
359            if rel:
360                name = rel
361            name = name.replace(os.sep, SEP)
362        self.fspath = fspath
363
364        session = session or parent.session
365
366        if nodeid is None:
367            nodeid = self.fspath.relto(session.config.rootdir)
368
369            if not nodeid:
370                nodeid = _check_initialpaths_for_relpath(session, fspath)
371            if nodeid and os.sep != SEP:
372                nodeid = nodeid.replace(os.sep, SEP)
373
374        super(FSCollector, self).__init__(
375            name, parent, config, session, nodeid=nodeid, fspath=fspath
376        )
377
378
379class File(FSCollector):
380    """ base class for collecting tests from a file. """
381
382
383class Item(Node):
384    """ a basic test invocation item. Note that for a single function
385    there might be multiple test invocation items.
386    """
387
388    nextitem = None
389
390    def __init__(self, name, parent=None, config=None, session=None, nodeid=None):
391        super(Item, self).__init__(name, parent, config, session, nodeid=nodeid)
392        self._report_sections = []
393
394        #: user properties is a list of tuples (name, value) that holds user
395        #: defined properties for this test.
396        self.user_properties = []
397
398    def add_report_section(self, when, key, content):
399        """
400        Adds a new report section, similar to what's done internally to add stdout and
401        stderr captured output::
402
403            item.add_report_section("call", "stdout", "report section contents")
404
405        :param str when:
406            One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``.
407        :param str key:
408            Name of the section, can be customized at will. Pytest uses ``"stdout"`` and
409            ``"stderr"`` internally.
410
411        :param str content:
412            The full contents as a string.
413        """
414        if content:
415            self._report_sections.append((when, key, content))
416
417    def reportinfo(self):
418        return self.fspath, None, ""
419
420    @property
421    def location(self):
422        try:
423            return self._location
424        except AttributeError:
425            location = self.reportinfo()
426            fspath = self.session._node_location_to_relpath(location[0])
427            location = (fspath, location[1], str(location[2]))
428            self._location = location
429            return location
430