1""" core implementation of testing process: init, session, runtest loop. """ 2import imp 3import os 4import re 5import sys 6 7import _pytest 8import _pytest._code 9import py 10import pytest 11try: 12 from collections import MutableMapping as MappingMixin 13except ImportError: 14 from UserDict import DictMixin as MappingMixin 15 16from _pytest.runner import collect_one_node 17 18tracebackcutdir = py.path.local(_pytest.__file__).dirpath() 19 20# exitcodes for the command line 21EXIT_OK = 0 22EXIT_TESTSFAILED = 1 23EXIT_INTERRUPTED = 2 24EXIT_INTERNALERROR = 3 25EXIT_USAGEERROR = 4 26EXIT_NOTESTSCOLLECTED = 5 27 28name_re = re.compile("^[a-zA-Z_]\w*$") 29 30def pytest_addoption(parser): 31 parser.addini("norecursedirs", "directory patterns to avoid for recursion", 32 type="args", default=['.*', 'CVS', '_darcs', '{arch}', '*.egg']) 33 parser.addini("testpaths", "directories to search for tests when no files or directories are given in the command line.", 34 type="args", default=[]) 35 #parser.addini("dirpatterns", 36 # "patterns specifying possible locations of test files", 37 # type="linelist", default=["**/test_*.txt", 38 # "**/test_*.py", "**/*_test.py"] 39 #) 40 group = parser.getgroup("general", "running and selection options") 41 group._addoption('-x', '--exitfirst', action="store_true", default=False, 42 dest="exitfirst", 43 help="exit instantly on first error or failed test."), 44 group._addoption('--maxfail', metavar="num", 45 action="store", type=int, dest="maxfail", default=0, 46 help="exit after first num failures or errors.") 47 group._addoption('--strict', action="store_true", 48 help="run pytest in strict mode, warnings become errors.") 49 group._addoption("-c", metavar="file", type=str, dest="inifilename", 50 help="load configuration from `file` instead of trying to locate one of the implicit configuration files.") 51 52 group = parser.getgroup("collect", "collection") 53 group.addoption('--collectonly', '--collect-only', action="store_true", 54 help="only collect tests, don't execute them."), 55 group.addoption('--pyargs', action="store_true", 56 help="try to interpret all arguments as python packages.") 57 group.addoption("--ignore", action="append", metavar="path", 58 help="ignore path during collection (multi-allowed).") 59 # when changing this to --conf-cut-dir, config.py Conftest.setinitial 60 # needs upgrading as well 61 group.addoption('--confcutdir', dest="confcutdir", default=None, 62 metavar="dir", 63 help="only load conftest.py's relative to specified dir.") 64 group.addoption('--noconftest', action="store_true", 65 dest="noconftest", default=False, 66 help="Don't load any conftest.py files.") 67 68 group = parser.getgroup("debugconfig", 69 "test session debugging and configuration") 70 group.addoption('--basetemp', dest="basetemp", default=None, metavar="dir", 71 help="base temporary directory for this test run.") 72 73 74def pytest_namespace(): 75 collect = dict(Item=Item, Collector=Collector, File=File, Session=Session) 76 return dict(collect=collect) 77 78def pytest_configure(config): 79 pytest.config = config # compatibiltiy 80 if config.option.exitfirst: 81 config.option.maxfail = 1 82 83def wrap_session(config, doit): 84 """Skeleton command line program""" 85 session = Session(config) 86 session.exitstatus = EXIT_OK 87 initstate = 0 88 try: 89 try: 90 config._do_configure() 91 initstate = 1 92 config.hook.pytest_sessionstart(session=session) 93 initstate = 2 94 session.exitstatus = doit(config, session) or 0 95 except pytest.UsageError: 96 raise 97 except KeyboardInterrupt: 98 excinfo = _pytest._code.ExceptionInfo() 99 config.hook.pytest_keyboard_interrupt(excinfo=excinfo) 100 session.exitstatus = EXIT_INTERRUPTED 101 except: 102 excinfo = _pytest._code.ExceptionInfo() 103 config.notify_exception(excinfo, config.option) 104 session.exitstatus = EXIT_INTERNALERROR 105 if excinfo.errisinstance(SystemExit): 106 sys.stderr.write("mainloop: caught Spurious SystemExit!\n") 107 108 finally: 109 excinfo = None # Explicitly break reference cycle. 110 session.startdir.chdir() 111 if initstate >= 2: 112 config.hook.pytest_sessionfinish( 113 session=session, 114 exitstatus=session.exitstatus) 115 config._ensure_unconfigure() 116 return session.exitstatus 117 118def pytest_cmdline_main(config): 119 return wrap_session(config, _main) 120 121def _main(config, session): 122 """ default command line protocol for initialization, session, 123 running tests and reporting. """ 124 config.hook.pytest_collection(session=session) 125 config.hook.pytest_runtestloop(session=session) 126 127 if session.testsfailed: 128 return EXIT_TESTSFAILED 129 elif session.testscollected == 0: 130 return EXIT_NOTESTSCOLLECTED 131 132def pytest_collection(session): 133 return session.perform_collect() 134 135def pytest_runtestloop(session): 136 if session.config.option.collectonly: 137 return True 138 139 def getnextitem(i): 140 # this is a function to avoid python2 141 # keeping sys.exc_info set when calling into a test 142 # python2 keeps sys.exc_info till the frame is left 143 try: 144 return session.items[i+1] 145 except IndexError: 146 return None 147 148 for i, item in enumerate(session.items): 149 nextitem = getnextitem(i) 150 item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem) 151 if session.shouldstop: 152 raise session.Interrupted(session.shouldstop) 153 return True 154 155def pytest_ignore_collect(path, config): 156 p = path.dirpath() 157 ignore_paths = config._getconftest_pathlist("collect_ignore", path=p) 158 ignore_paths = ignore_paths or [] 159 excludeopt = config.getoption("ignore") 160 if excludeopt: 161 ignore_paths.extend([py.path.local(x) for x in excludeopt]) 162 return path in ignore_paths 163 164class FSHookProxy: 165 def __init__(self, fspath, pm, remove_mods): 166 self.fspath = fspath 167 self.pm = pm 168 self.remove_mods = remove_mods 169 170 def __getattr__(self, name): 171 x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods) 172 self.__dict__[name] = x 173 return x 174 175def compatproperty(name): 176 def fget(self): 177 # deprecated - use pytest.name 178 return getattr(pytest, name) 179 180 return property(fget) 181 182class NodeKeywords(MappingMixin): 183 def __init__(self, node): 184 self.node = node 185 self.parent = node.parent 186 self._markers = {node.name: True} 187 188 def __getitem__(self, key): 189 try: 190 return self._markers[key] 191 except KeyError: 192 if self.parent is None: 193 raise 194 return self.parent.keywords[key] 195 196 def __setitem__(self, key, value): 197 self._markers[key] = value 198 199 def __delitem__(self, key): 200 raise ValueError("cannot delete key in keywords dict") 201 202 def __iter__(self): 203 seen = set(self._markers) 204 if self.parent is not None: 205 seen.update(self.parent.keywords) 206 return iter(seen) 207 208 def __len__(self): 209 return len(self.__iter__()) 210 211 def keys(self): 212 return list(self) 213 214 def __repr__(self): 215 return "<NodeKeywords for node %s>" % (self.node, ) 216 217 218class Node(object): 219 """ base class for Collector and Item the test collection tree. 220 Collector subclasses have children, Items are terminal nodes.""" 221 222 def __init__(self, name, parent=None, config=None, session=None): 223 #: a unique name within the scope of the parent node 224 self.name = name 225 226 #: the parent collector node. 227 self.parent = parent 228 229 #: the pytest config object 230 self.config = config or parent.config 231 232 #: the session this node is part of 233 self.session = session or parent.session 234 235 #: filesystem path where this node was collected from (can be None) 236 self.fspath = getattr(parent, 'fspath', None) 237 238 #: keywords/markers collected from all scopes 239 self.keywords = NodeKeywords(self) 240 241 #: allow adding of extra keywords to use for matching 242 self.extra_keyword_matches = set() 243 244 # used for storing artificial fixturedefs for direct parametrization 245 self._name2pseudofixturedef = {} 246 247 @property 248 def ihook(self): 249 """ fspath sensitive hook proxy used to call pytest hooks""" 250 return self.session.gethookproxy(self.fspath) 251 252 Module = compatproperty("Module") 253 Class = compatproperty("Class") 254 Instance = compatproperty("Instance") 255 Function = compatproperty("Function") 256 File = compatproperty("File") 257 Item = compatproperty("Item") 258 259 def _getcustomclass(self, name): 260 cls = getattr(self, name) 261 if cls != getattr(pytest, name): 262 py.log._apiwarn("2.0", "use of node.%s is deprecated, " 263 "use pytest_pycollect_makeitem(...) to create custom " 264 "collection nodes" % name) 265 return cls 266 267 def __repr__(self): 268 return "<%s %r>" %(self.__class__.__name__, 269 getattr(self, 'name', None)) 270 271 def warn(self, code, message): 272 """ generate a warning with the given code and message for this 273 item. """ 274 assert isinstance(code, str) 275 fslocation = getattr(self, "location", None) 276 if fslocation is None: 277 fslocation = getattr(self, "fspath", None) 278 else: 279 fslocation = "%s:%s" % fslocation[:2] 280 281 self.ihook.pytest_logwarning.call_historic(kwargs=dict( 282 code=code, message=message, 283 nodeid=self.nodeid, fslocation=fslocation)) 284 285 # methods for ordering nodes 286 @property 287 def nodeid(self): 288 """ a ::-separated string denoting its collection tree address. """ 289 try: 290 return self._nodeid 291 except AttributeError: 292 self._nodeid = x = self._makeid() 293 return x 294 295 def _makeid(self): 296 return self.parent.nodeid + "::" + self.name 297 298 def __hash__(self): 299 return hash(self.nodeid) 300 301 def setup(self): 302 pass 303 304 def teardown(self): 305 pass 306 307 def _memoizedcall(self, attrname, function): 308 exattrname = "_ex_" + attrname 309 failure = getattr(self, exattrname, None) 310 if failure is not None: 311 py.builtin._reraise(failure[0], failure[1], failure[2]) 312 if hasattr(self, attrname): 313 return getattr(self, attrname) 314 try: 315 res = function() 316 except py.builtin._sysex: 317 raise 318 except: 319 failure = sys.exc_info() 320 setattr(self, exattrname, failure) 321 raise 322 setattr(self, attrname, res) 323 return res 324 325 def listchain(self): 326 """ return list of all parent collectors up to self, 327 starting from root of collection tree. """ 328 chain = [] 329 item = self 330 while item is not None: 331 chain.append(item) 332 item = item.parent 333 chain.reverse() 334 return chain 335 336 def add_marker(self, marker): 337 """ dynamically add a marker object to the node. 338 339 ``marker`` can be a string or pytest.mark.* instance. 340 """ 341 from _pytest.mark import MarkDecorator 342 if isinstance(marker, py.builtin._basestring): 343 marker = MarkDecorator(marker) 344 elif not isinstance(marker, MarkDecorator): 345 raise ValueError("is not a string or pytest.mark.* Marker") 346 self.keywords[marker.name] = marker 347 348 def get_marker(self, name): 349 """ get a marker object from this node or None if 350 the node doesn't have a marker with that name. """ 351 val = self.keywords.get(name, None) 352 if val is not None: 353 from _pytest.mark import MarkInfo, MarkDecorator 354 if isinstance(val, (MarkDecorator, MarkInfo)): 355 return val 356 357 def listextrakeywords(self): 358 """ Return a set of all extra keywords in self and any parents.""" 359 extra_keywords = set() 360 item = self 361 for item in self.listchain(): 362 extra_keywords.update(item.extra_keyword_matches) 363 return extra_keywords 364 365 def listnames(self): 366 return [x.name for x in self.listchain()] 367 368 def addfinalizer(self, fin): 369 """ register a function to be called when this node is finalized. 370 371 This method can only be called when this node is active 372 in a setup chain, for example during self.setup(). 373 """ 374 self.session._setupstate.addfinalizer(fin, self) 375 376 def getparent(self, cls): 377 """ get the next parent node (including ourself) 378 which is an instance of the given class""" 379 current = self 380 while current and not isinstance(current, cls): 381 current = current.parent 382 return current 383 384 def _prunetraceback(self, excinfo): 385 pass 386 387 def _repr_failure_py(self, excinfo, style=None): 388 fm = self.session._fixturemanager 389 if excinfo.errisinstance(fm.FixtureLookupError): 390 return excinfo.value.formatrepr() 391 tbfilter = True 392 if self.config.option.fulltrace: 393 style="long" 394 else: 395 self._prunetraceback(excinfo) 396 tbfilter = False # prunetraceback already does it 397 if style == "auto": 398 style = "long" 399 # XXX should excinfo.getrepr record all data and toterminal() process it? 400 if style is None: 401 if self.config.option.tbstyle == "short": 402 style = "short" 403 else: 404 style = "long" 405 406 return excinfo.getrepr(funcargs=True, 407 showlocals=self.config.option.showlocals, 408 style=style, tbfilter=tbfilter) 409 410 repr_failure = _repr_failure_py 411 412class Collector(Node): 413 """ Collector instances create children through collect() 414 and thus iteratively build a tree. 415 """ 416 417 class CollectError(Exception): 418 """ an error during collection, contains a custom message. """ 419 420 def collect(self): 421 """ returns a list of children (items and collectors) 422 for this collection node. 423 """ 424 raise NotImplementedError("abstract") 425 426 def repr_failure(self, excinfo): 427 """ represent a collection failure. """ 428 if excinfo.errisinstance(self.CollectError): 429 exc = excinfo.value 430 return str(exc.args[0]) 431 return self._repr_failure_py(excinfo, style="short") 432 433 def _memocollect(self): 434 """ internal helper method to cache results of calling collect(). """ 435 return self._memoizedcall('_collected', lambda: list(self.collect())) 436 437 def _prunetraceback(self, excinfo): 438 if hasattr(self, 'fspath'): 439 traceback = excinfo.traceback 440 ntraceback = traceback.cut(path=self.fspath) 441 if ntraceback == traceback: 442 ntraceback = ntraceback.cut(excludepath=tracebackcutdir) 443 excinfo.traceback = ntraceback.filter() 444 445class FSCollector(Collector): 446 def __init__(self, fspath, parent=None, config=None, session=None): 447 fspath = py.path.local(fspath) # xxx only for test_resultlog.py? 448 name = fspath.basename 449 if parent is not None: 450 rel = fspath.relto(parent.fspath) 451 if rel: 452 name = rel 453 name = name.replace(os.sep, "/") 454 super(FSCollector, self).__init__(name, parent, config, session) 455 self.fspath = fspath 456 457 def _makeid(self): 458 relpath = self.fspath.relto(self.config.rootdir) 459 if os.sep != "/": 460 relpath = relpath.replace(os.sep, "/") 461 return relpath 462 463class File(FSCollector): 464 """ base class for collecting tests from a file. """ 465 466class Item(Node): 467 """ a basic test invocation item. Note that for a single function 468 there might be multiple test invocation items. 469 """ 470 nextitem = None 471 472 def __init__(self, name, parent=None, config=None, session=None): 473 super(Item, self).__init__(name, parent, config, session) 474 self._report_sections = [] 475 476 def add_report_section(self, when, key, content): 477 if content: 478 self._report_sections.append((when, key, content)) 479 480 def reportinfo(self): 481 return self.fspath, None, "" 482 483 @property 484 def location(self): 485 try: 486 return self._location 487 except AttributeError: 488 location = self.reportinfo() 489 # bestrelpath is a quite slow function 490 cache = self.config.__dict__.setdefault("_bestrelpathcache", {}) 491 try: 492 fspath = cache[location[0]] 493 except KeyError: 494 fspath = self.session.fspath.bestrelpath(location[0]) 495 cache[location[0]] = fspath 496 location = (fspath, location[1], str(location[2])) 497 self._location = location 498 return location 499 500class NoMatch(Exception): 501 """ raised if matching cannot locate a matching names. """ 502 503class Interrupted(KeyboardInterrupt): 504 """ signals an interrupted test run. """ 505 __module__ = 'builtins' # for py3 506 507class Session(FSCollector): 508 Interrupted = Interrupted 509 510 def __init__(self, config): 511 FSCollector.__init__(self, config.rootdir, parent=None, 512 config=config, session=self) 513 self._fs2hookproxy = {} 514 self.testsfailed = 0 515 self.testscollected = 0 516 self.shouldstop = False 517 self.trace = config.trace.root.get("collection") 518 self._norecursepatterns = config.getini("norecursedirs") 519 self.startdir = py.path.local() 520 self.config.pluginmanager.register(self, name="session") 521 522 def _makeid(self): 523 return "" 524 525 @pytest.hookimpl(tryfirst=True) 526 def pytest_collectstart(self): 527 if self.shouldstop: 528 raise self.Interrupted(self.shouldstop) 529 530 @pytest.hookimpl(tryfirst=True) 531 def pytest_runtest_logreport(self, report): 532 if report.failed and not hasattr(report, 'wasxfail'): 533 self.testsfailed += 1 534 maxfail = self.config.getvalue("maxfail") 535 if maxfail and self.testsfailed >= maxfail: 536 self.shouldstop = "stopping after %d failures" % ( 537 self.testsfailed) 538 pytest_collectreport = pytest_runtest_logreport 539 540 def isinitpath(self, path): 541 return path in self._initialpaths 542 543 def gethookproxy(self, fspath): 544 try: 545 return self._fs2hookproxy[fspath] 546 except KeyError: 547 # check if we have the common case of running 548 # hooks with all conftest.py filesall conftest.py 549 pm = self.config.pluginmanager 550 my_conftestmodules = pm._getconftestmodules(fspath) 551 remove_mods = pm._conftest_plugins.difference(my_conftestmodules) 552 if remove_mods: 553 # one or more conftests are not in use at this fspath 554 proxy = FSHookProxy(fspath, pm, remove_mods) 555 else: 556 # all plugis are active for this fspath 557 proxy = self.config.hook 558 559 self._fs2hookproxy[fspath] = proxy 560 return proxy 561 562 def perform_collect(self, args=None, genitems=True): 563 hook = self.config.hook 564 try: 565 items = self._perform_collect(args, genitems) 566 hook.pytest_collection_modifyitems(session=self, 567 config=self.config, items=items) 568 finally: 569 hook.pytest_collection_finish(session=self) 570 self.testscollected = len(items) 571 return items 572 573 def _perform_collect(self, args, genitems): 574 if args is None: 575 args = self.config.args 576 self.trace("perform_collect", self, args) 577 self.trace.root.indent += 1 578 self._notfound = [] 579 self._initialpaths = set() 580 self._initialparts = [] 581 self.items = items = [] 582 for arg in args: 583 parts = self._parsearg(arg) 584 self._initialparts.append(parts) 585 self._initialpaths.add(parts[0]) 586 rep = collect_one_node(self) 587 self.ihook.pytest_collectreport(report=rep) 588 self.trace.root.indent -= 1 589 if self._notfound: 590 errors = [] 591 for arg, exc in self._notfound: 592 line = "(no name %r in any of %r)" % (arg, exc.args[0]) 593 errors.append("not found: %s\n%s" % (arg, line)) 594 #XXX: test this 595 raise pytest.UsageError(*errors) 596 if not genitems: 597 return rep.result 598 else: 599 if rep.passed: 600 for node in rep.result: 601 self.items.extend(self.genitems(node)) 602 return items 603 604 def collect(self): 605 for parts in self._initialparts: 606 arg = "::".join(map(str, parts)) 607 self.trace("processing argument", arg) 608 self.trace.root.indent += 1 609 try: 610 for x in self._collect(arg): 611 yield x 612 except NoMatch: 613 # we are inside a make_report hook so 614 # we cannot directly pass through the exception 615 self._notfound.append((arg, sys.exc_info()[1])) 616 617 self.trace.root.indent -= 1 618 619 def _collect(self, arg): 620 names = self._parsearg(arg) 621 path = names.pop(0) 622 if path.check(dir=1): 623 assert not names, "invalid arg %r" %(arg,) 624 for path in path.visit(fil=lambda x: x.check(file=1), 625 rec=self._recurse, bf=True, sort=True): 626 for x in self._collectfile(path): 627 yield x 628 else: 629 assert path.check(file=1) 630 for x in self.matchnodes(self._collectfile(path), names): 631 yield x 632 633 def _collectfile(self, path): 634 ihook = self.gethookproxy(path) 635 if not self.isinitpath(path): 636 if ihook.pytest_ignore_collect(path=path, config=self.config): 637 return () 638 return ihook.pytest_collect_file(path=path, parent=self) 639 640 def _recurse(self, path): 641 ihook = self.gethookproxy(path.dirpath()) 642 if ihook.pytest_ignore_collect(path=path, config=self.config): 643 return 644 for pat in self._norecursepatterns: 645 if path.check(fnmatch=pat): 646 return False 647 ihook = self.gethookproxy(path) 648 ihook.pytest_collect_directory(path=path, parent=self) 649 return True 650 651 def _tryconvertpyarg(self, x): 652 mod = None 653 path = [os.path.abspath('.')] + sys.path 654 for name in x.split('.'): 655 # ignore anything that's not a proper name here 656 # else something like --pyargs will mess up '.' 657 # since imp.find_module will actually sometimes work for it 658 # but it's supposed to be considered a filesystem path 659 # not a package 660 if name_re.match(name) is None: 661 return x 662 try: 663 fd, mod, type_ = imp.find_module(name, path) 664 except ImportError: 665 return x 666 else: 667 if fd is not None: 668 fd.close() 669 670 if type_[2] != imp.PKG_DIRECTORY: 671 path = [os.path.dirname(mod)] 672 else: 673 path = [mod] 674 return mod 675 676 def _parsearg(self, arg): 677 """ return (fspath, names) tuple after checking the file exists. """ 678 arg = str(arg) 679 if self.config.option.pyargs: 680 arg = self._tryconvertpyarg(arg) 681 parts = str(arg).split("::") 682 relpath = parts[0].replace("/", os.sep) 683 path = self.config.invocation_dir.join(relpath, abs=True) 684 if not path.check(): 685 if self.config.option.pyargs: 686 msg = "file or package not found: " 687 else: 688 msg = "file not found: " 689 raise pytest.UsageError(msg + arg) 690 parts[0] = path 691 return parts 692 693 def matchnodes(self, matching, names): 694 self.trace("matchnodes", matching, names) 695 self.trace.root.indent += 1 696 nodes = self._matchnodes(matching, names) 697 num = len(nodes) 698 self.trace("matchnodes finished -> ", num, "nodes") 699 self.trace.root.indent -= 1 700 if num == 0: 701 raise NoMatch(matching, names[:1]) 702 return nodes 703 704 def _matchnodes(self, matching, names): 705 if not matching or not names: 706 return matching 707 name = names[0] 708 assert name 709 nextnames = names[1:] 710 resultnodes = [] 711 for node in matching: 712 if isinstance(node, pytest.Item): 713 if not names: 714 resultnodes.append(node) 715 continue 716 assert isinstance(node, pytest.Collector) 717 rep = collect_one_node(node) 718 if rep.passed: 719 has_matched = False 720 for x in rep.result: 721 # TODO: remove parametrized workaround once collection structure contains parametrization 722 if x.name == name or x.name.split("[")[0] == name: 723 resultnodes.extend(self.matchnodes([x], nextnames)) 724 has_matched = True 725 # XXX accept IDs that don't have "()" for class instances 726 if not has_matched and len(rep.result) == 1 and x.name == "()": 727 nextnames.insert(0, name) 728 resultnodes.extend(self.matchnodes([x], nextnames)) 729 node.ihook.pytest_collectreport(report=rep) 730 return resultnodes 731 732 def genitems(self, node): 733 self.trace("genitems", node) 734 if isinstance(node, pytest.Item): 735 node.ihook.pytest_itemcollected(item=node) 736 yield node 737 else: 738 assert isinstance(node, pytest.Collector) 739 rep = collect_one_node(node) 740 if rep.passed: 741 for subnode in rep.result: 742 for x in self.genitems(subnode): 743 yield x 744 node.ihook.pytest_collectreport(report=rep) 745