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