1
""" core implementation of testing process: init, session, runtest loop. """
11
from collections import MutableMapping as MappingMixin
13
from UserDict import DictMixin as MappingMixin
15
from _pytest.config import directory_arg
16
from _pytest.runner import collect_one_node
18
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
20
# exitcodes for the command line
24
EXIT_INTERNALERROR = 3
26
EXIT_NOTESTSCOLLECTED = 5
28
def pytest_addoption(parser):
29
parser.addini("norecursedirs", "directory patterns to avoid for recursion",
30
type="args", default=['.*', 'build', 'dist', 'CVS', '_darcs', '{arch}', '*.egg'])
31
parser.addini("testpaths", "directories to search for tests when no files or directories are given in the command line.",
32
type="args", default=[])
33
#parser.addini("dirpatterns",
34
# "patterns specifying possible locations of test files",
35
# type="linelist", default=["**/test_*.txt",
36
# "**/test_*.py", "**/*_test.py"]
38
group = parser.getgroup("general", "running and selection options")
39
group._addoption('-x', '--exitfirst', action="store_const",
40
dest="maxfail", const=1,
41
help="exit instantly on first error or failed test."),
42
group._addoption('--maxfail', metavar="num",
43
action="store", type=int, dest="maxfail", default=0,
44
help="exit after first num failures or errors.")
45
group._addoption('--strict', action="store_true",
46
help="run pytest in strict mode, warnings become errors.")
47
group._addoption("-c", metavar="file", type=str, dest="inifilename",
48
help="load configuration from `file` instead of trying to locate one of the implicit configuration files.")
49
group._addoption("--continue-on-collection-errors", action="store_true",
50
default=False, dest="continue_on_collection_errors",
51
help="Force test execution even if collection errors occur.")
53
group = parser.getgroup("collect", "collection")
54
group.addoption('--collectonly', '--collect-only', action="store_true",
55
help="only collect tests, don't execute them."),
56
group.addoption('--pyargs', action="store_true",
57
help="try to interpret all arguments as python packages.")
58
group.addoption("--ignore", action="append", metavar="path",
59
help="ignore path during collection (multi-allowed).")
60
# when changing this to --conf-cut-dir, config.py Conftest.setinitial
61
# needs upgrading as well
62
group.addoption('--confcutdir', dest="confcutdir", default=None,
63
metavar="dir", type=functools.partial(directory_arg, optname="--confcutdir"),
64
help="only load conftest.py's relative to specified dir.")
65
group.addoption('--noconftest', action="store_true",
66
dest="noconftest", default=False,
67
help="Don't load any conftest.py files.")
68
group.addoption('--keepduplicates', '--keep-duplicates', action="store_true",
69
dest="keepduplicates", default=False,
70
help="Keep duplicate tests.")
72
group = parser.getgroup("debugconfig",
73
"test session debugging and configuration")
74
group.addoption('--basetemp', dest="basetemp", default=None, metavar="dir",
75
help="base temporary directory for this test run.")
78
def pytest_namespace():
79
collect = dict(Item=Item, Collector=Collector, File=File, Session=Session)
80
return dict(collect=collect)
83
def pytest_configure(config):
84
pytest.config = config # compatibiltiy
87
def wrap_session(config, doit):
88
"""Skeleton command line program"""
89
session = Session(config)
90
session.exitstatus = EXIT_OK
94
config._do_configure()
96
config.hook.pytest_sessionstart(session=session)
98
session.exitstatus = doit(config, session) or 0
99
except pytest.UsageError:
101
except KeyboardInterrupt:
102
excinfo = _pytest._code.ExceptionInfo()
103
if initstate < 2 and isinstance(
104
excinfo.value, pytest.exit.Exception):
105
sys.stderr.write('{0}: {1}\n'.format(
106
excinfo.typename, excinfo.value.msg))
107
config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
108
session.exitstatus = EXIT_INTERRUPTED
110
excinfo = _pytest._code.ExceptionInfo()
111
config.notify_exception(excinfo, config.option)
112
session.exitstatus = EXIT_INTERNALERROR
113
if excinfo.errisinstance(SystemExit):
114
sys.stderr.write("mainloop: caught Spurious SystemExit!\n")
117
excinfo = None # Explicitly break reference cycle.
118
session.startdir.chdir()
120
config.hook.pytest_sessionfinish(
122
exitstatus=session.exitstatus)
123
config._ensure_unconfigure()
124
return session.exitstatus
126
def pytest_cmdline_main(config):
127
return wrap_session(config, _main)
129
def _main(config, session):
130
""" default command line protocol for initialization, session,
131
running tests and reporting. """
132
config.hook.pytest_collection(session=session)
133
config.hook.pytest_runtestloop(session=session)
135
if session.testsfailed:
136
return EXIT_TESTSFAILED
137
elif session.testscollected == 0:
138
return EXIT_NOTESTSCOLLECTED
140
def pytest_collection(session):
141
return session.perform_collect()
143
def pytest_runtestloop(session):
144
if (session.testsfailed and
145
not session.config.option.continue_on_collection_errors):
146
raise session.Interrupted(
147
"%d errors during collection" % session.testsfailed)
149
if session.config.option.collectonly:
152
for i, item in enumerate(session.items):
153
nextitem = session.items[i+1] if i+1 < len(session.items) else None
154
item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
155
if session.shouldstop:
156
raise session.Interrupted(session.shouldstop)
159
def pytest_ignore_collect(path, config):
161
ignore_paths = config._getconftest_pathlist("collect_ignore", path=p)
162
ignore_paths = ignore_paths or []
163
excludeopt = config.getoption("ignore")
165
ignore_paths.extend([py.path.local(x) for x in excludeopt])
167
if path in ignore_paths:
170
# Skip duplicate paths.
171
keepduplicates = config.getoption("keepduplicates")
172
duplicate_paths = config.pluginmanager._duplicatepaths
173
if not keepduplicates:
174
if path in duplicate_paths:
177
duplicate_paths.add(path)
183
def __init__(self, fspath, pm, remove_mods):
186
self.remove_mods = remove_mods
188
def __getattr__(self, name):
189
x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
190
self.__dict__[name] = x
193
class _CompatProperty(object):
194
def __init__(self, name):
197
def __get__(self, obj, owner):
201
# TODO: reenable in the features branch
203
# "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format(
204
# name=self.name, owner=type(owner).__name__),
205
# PendingDeprecationWarning, stacklevel=2)
206
return getattr(pytest, self.name)
210
class NodeKeywords(MappingMixin):
211
def __init__(self, node):
213
self.parent = node.parent
214
self._markers = {node.name: True}
216
def __getitem__(self, key):
218
return self._markers[key]
220
if self.parent is None:
222
return self.parent.keywords[key]
224
def __setitem__(self, key, value):
225
self._markers[key] = value
227
def __delitem__(self, key):
228
raise ValueError("cannot delete key in keywords dict")
231
seen = set(self._markers)
232
if self.parent is not None:
233
seen.update(self.parent.keywords)
237
return len(self.__iter__())
243
return "<NodeKeywords for node %s>" % (self.node, )
247
""" base class for Collector and Item the test collection tree.
248
Collector subclasses have children, Items are terminal nodes."""
250
def __init__(self, name, parent=None, config=None, session=None):
251
#: a unique name within the scope of the parent node
254
#: the parent collector node.
257
#: the pytest config object
258
self.config = config or parent.config
260
#: the session this node is part of
261
self.session = session or parent.session
263
#: filesystem path where this node was collected from (can be None)
264
self.fspath = getattr(parent, 'fspath', None)
266
#: keywords/markers collected from all scopes
267
self.keywords = NodeKeywords(self)
269
#: allow adding of extra keywords to use for matching
270
self.extra_keyword_matches = set()
272
# used for storing artificial fixturedefs for direct parametrization
273
self._name2pseudofixturedef = {}
277
""" fspath sensitive hook proxy used to call pytest hooks"""
278
return self.session.gethookproxy(self.fspath)
280
Module = _CompatProperty("Module")
281
Class = _CompatProperty("Class")
282
Instance = _CompatProperty("Instance")
283
Function = _CompatProperty("Function")
284
File = _CompatProperty("File")
285
Item = _CompatProperty("Item")
287
def _getcustomclass(self, name):
288
maybe_compatprop = getattr(type(self), name)
289
if isinstance(maybe_compatprop, _CompatProperty):
290
return getattr(pytest, name)
292
cls = getattr(self, name)
293
# TODO: reenable in the features branch
294
# warnings.warn("use of node.%s is deprecated, "
295
# "use pytest_pycollect_makeitem(...) to create custom "
296
# "collection nodes" % name, category=DeprecationWarning)
300
return "<%s %r>" %(self.__class__.__name__,
301
getattr(self, 'name', None))
303
def warn(self, code, message):
304
""" generate a warning with the given code and message for this
306
assert isinstance(code, str)
307
fslocation = getattr(self, "location", None)
308
if fslocation is None:
309
fslocation = getattr(self, "fspath", None)
311
fslocation = "%s:%s" % (fslocation[0], fslocation[1] + 1)
313
self.ihook.pytest_logwarning.call_historic(kwargs=dict(
314
code=code, message=message,
315
nodeid=self.nodeid, fslocation=fslocation))
317
# methods for ordering nodes
320
""" a ::-separated string denoting its collection tree address. """
323
except AttributeError:
324
self._nodeid = x = self._makeid()
328
return self.parent.nodeid + "::" + self.name
331
return hash(self.nodeid)
339
def _memoizedcall(self, attrname, function):
340
exattrname = "_ex_" + attrname
341
failure = getattr(self, exattrname, None)
342
if failure is not None:
343
py.builtin._reraise(failure[0], failure[1], failure[2])
344
if hasattr(self, attrname):
345
return getattr(self, attrname)
348
except py.builtin._sysex:
351
failure = sys.exc_info()
352
setattr(self, exattrname, failure)
354
setattr(self, attrname, res)
358
""" return list of all parent collectors up to self,
359
starting from root of collection tree. """
362
while item is not None:
368
def add_marker(self, marker):
369
""" dynamically add a marker object to the node.
371
``marker`` can be a string or pytest.mark.* instance.
373
from _pytest.mark import MarkDecorator
374
if isinstance(marker, py.builtin._basestring):
375
marker = MarkDecorator(marker)
376
elif not isinstance(marker, MarkDecorator):
377
raise ValueError("is not a string or pytest.mark.* Marker")
378
self.keywords[marker.name] = marker
380
def get_marker(self, name):
381
""" get a marker object from this node or None if
382
the node doesn't have a marker with that name. """
383
val = self.keywords.get(name, None)
385
from _pytest.mark import MarkInfo, MarkDecorator
386
if isinstance(val, (MarkDecorator, MarkInfo)):
389
def listextrakeywords(self):
390
""" Return a set of all extra keywords in self and any parents."""
391
extra_keywords = set()
393
for item in self.listchain():
394
extra_keywords.update(item.extra_keyword_matches)
395
return extra_keywords
398
return [x.name for x in self.listchain()]
400
def addfinalizer(self, fin):
401
""" register a function to be called when this node is finalized.
403
This method can only be called when this node is active
404
in a setup chain, for example during self.setup().
406
self.session._setupstate.addfinalizer(fin, self)
408
def getparent(self, cls):
409
""" get the next parent node (including ourself)
410
which is an instance of the given class"""
412
while current and not isinstance(current, cls):
413
current = current.parent
416
def _prunetraceback(self, excinfo):
419
def _repr_failure_py(self, excinfo, style=None):
420
fm = self.session._fixturemanager
421
if excinfo.errisinstance(fm.FixtureLookupError):
422
return excinfo.value.formatrepr()
424
if self.config.option.fulltrace:
427
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
428
self._prunetraceback(excinfo)
429
if len(excinfo.traceback) == 0:
430
excinfo.traceback = tb
431
tbfilter = False # prunetraceback already does it
434
# XXX should excinfo.getrepr record all data and toterminal() process it?
436
if self.config.option.tbstyle == "short":
447
return excinfo.getrepr(funcargs=True, abspath=abspath,
448
showlocals=self.config.option.showlocals,
449
style=style, tbfilter=tbfilter)
451
repr_failure = _repr_failure_py
453
class Collector(Node):
454
""" Collector instances create children through collect()
455
and thus iteratively build a tree.
458
class CollectError(Exception):
459
""" an error during collection, contains a custom message. """
462
""" returns a list of children (items and collectors)
463
for this collection node.
465
raise NotImplementedError("abstract")
467
def repr_failure(self, excinfo):
468
""" represent a collection failure. """
469
if excinfo.errisinstance(self.CollectError):
471
return str(exc.args[0])
472
return self._repr_failure_py(excinfo, style="short")
474
def _memocollect(self):
475
""" internal helper method to cache results of calling collect(). """
476
return self._memoizedcall('_collected', lambda: list(self.collect()))
478
def _prunetraceback(self, excinfo):
479
if hasattr(self, 'fspath'):
480
traceback = excinfo.traceback
481
ntraceback = traceback.cut(path=self.fspath)
482
if ntraceback == traceback:
483
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
484
excinfo.traceback = ntraceback.filter()
486
class FSCollector(Collector):
487
def __init__(self, fspath, parent=None, config=None, session=None):
488
fspath = py.path.local(fspath) # xxx only for test_resultlog.py?
489
name = fspath.basename
490
if parent is not None:
491
rel = fspath.relto(parent.fspath)
494
name = name.replace(os.sep, "/")
495
super(FSCollector, self).__init__(name, parent, config, session)
499
relpath = self.fspath.relto(self.config.rootdir)
501
relpath = relpath.replace(os.sep, "/")
504
class File(FSCollector):
505
""" base class for collecting tests from a file. """
508
""" a basic test invocation item. Note that for a single function
509
there might be multiple test invocation items.
513
def __init__(self, name, parent=None, config=None, session=None):
514
super(Item, self).__init__(name, parent, config, session)
515
self._report_sections = []
517
def add_report_section(self, when, key, content):
519
self._report_sections.append((when, key, content))
521
def reportinfo(self):
522
return self.fspath, None, ""
527
return self._location
528
except AttributeError:
529
location = self.reportinfo()
530
# bestrelpath is a quite slow function
531
cache = self.config.__dict__.setdefault("_bestrelpathcache", {})
533
fspath = cache[location[0]]
535
fspath = self.session.fspath.bestrelpath(location[0])
536
cache[location[0]] = fspath
537
location = (fspath, location[1], str(location[2]))
538
self._location = location
541
class NoMatch(Exception):
542
""" raised if matching cannot locate a matching names. """
544
class Interrupted(KeyboardInterrupt):
545
""" signals an interrupted test run. """
546
__module__ = 'builtins' # for py3
548
class Session(FSCollector):
549
Interrupted = Interrupted
551
def __init__(self, config):
552
FSCollector.__init__(self, config.rootdir, parent=None,
553
config=config, session=self)
555
self.testscollected = 0
556
self.shouldstop = False
557
self.trace = config.trace.root.get("collection")
558
self._norecursepatterns = config.getini("norecursedirs")
559
self.startdir = py.path.local()
560
self.config.pluginmanager.register(self, name="session")
565
@pytest.hookimpl(tryfirst=True)
566
def pytest_collectstart(self):
568
raise self.Interrupted(self.shouldstop)
570
@pytest.hookimpl(tryfirst=True)
571
def pytest_runtest_logreport(self, report):
572
if report.failed and not hasattr(report, 'wasxfail'):
573
self.testsfailed += 1
574
maxfail = self.config.getvalue("maxfail")
575
if maxfail and self.testsfailed >= maxfail:
576
self.shouldstop = "stopping after %d failures" % (
578
pytest_collectreport = pytest_runtest_logreport
580
def isinitpath(self, path):
581
return path in self._initialpaths
583
def gethookproxy(self, fspath):
584
# check if we have the common case of running
585
# hooks with all conftest.py filesall conftest.py
586
pm = self.config.pluginmanager
587
my_conftestmodules = pm._getconftestmodules(fspath)
588
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
590
# one or more conftests are not in use at this fspath
591
proxy = FSHookProxy(fspath, pm, remove_mods)
593
# all plugis are active for this fspath
594
proxy = self.config.hook
597
def perform_collect(self, args=None, genitems=True):
598
hook = self.config.hook
600
items = self._perform_collect(args, genitems)
601
hook.pytest_collection_modifyitems(session=self,
602
config=self.config, items=items)
604
hook.pytest_collection_finish(session=self)
605
self.testscollected = len(items)
608
def _perform_collect(self, args, genitems):
610
args = self.config.args
611
self.trace("perform_collect", self, args)
612
self.trace.root.indent += 1
614
self._initialpaths = set()
615
self._initialparts = []
616
self.items = items = []
618
parts = self._parsearg(arg)
619
self._initialparts.append(parts)
620
self._initialpaths.add(parts[0])
621
rep = collect_one_node(self)
622
self.ihook.pytest_collectreport(report=rep)
623
self.trace.root.indent -= 1
626
for arg, exc in self._notfound:
627
line = "(no name %r in any of %r)" % (arg, exc.args[0])
628
errors.append("not found: %s\n%s" % (arg, line))
630
raise pytest.UsageError(*errors)
635
for node in rep.result:
636
self.items.extend(self.genitems(node))
640
for parts in self._initialparts:
641
arg = "::".join(map(str, parts))
642
self.trace("processing argument", arg)
643
self.trace.root.indent += 1
645
for x in self._collect(arg):
648
# we are inside a make_report hook so
649
# we cannot directly pass through the exception
650
self._notfound.append((arg, sys.exc_info()[1]))
652
self.trace.root.indent -= 1
654
def _collect(self, arg):
655
names = self._parsearg(arg)
657
if path.check(dir=1):
658
assert not names, "invalid arg %r" %(arg,)
659
for path in path.visit(fil=lambda x: x.check(file=1),
660
rec=self._recurse, bf=True, sort=True):
661
for x in self._collectfile(path):
664
assert path.check(file=1)
665
for x in self.matchnodes(self._collectfile(path), names):
668
def _collectfile(self, path):
669
ihook = self.gethookproxy(path)
670
if not self.isinitpath(path):
671
if ihook.pytest_ignore_collect(path=path, config=self.config):
673
return ihook.pytest_collect_file(path=path, parent=self)
675
def _recurse(self, path):
676
ihook = self.gethookproxy(path.dirpath())
677
if ihook.pytest_ignore_collect(path=path, config=self.config):
679
for pat in self._norecursepatterns:
680
if path.check(fnmatch=pat):
682
ihook = self.gethookproxy(path)
683
ihook.pytest_collect_directory(path=path, parent=self)
686
def _tryconvertpyarg(self, x):
687
"""Convert a dotted module name to path.
692
loader = pkgutil.find_loader(x)
697
# This method is sometimes invoked when AssertionRewritingHook, which
698
# does not define a get_filename method, is already in place:
700
path = loader.get_filename(x)
701
except AttributeError:
702
# Retrieve path from AssertionRewritingHook:
703
path = loader.modules[x][0].co_filename
704
if loader.is_package(x):
705
path = os.path.dirname(path)
708
def _parsearg(self, arg):
709
""" return (fspath, names) tuple after checking the file exists. """
710
parts = str(arg).split("::")
711
if self.config.option.pyargs:
712
parts[0] = self._tryconvertpyarg(parts[0])
713
relpath = parts[0].replace("/", os.sep)
714
path = self.config.invocation_dir.join(relpath, abs=True)
716
if self.config.option.pyargs:
717
raise pytest.UsageError("file or package not found: " + arg + " (missing __init__.py?)")
719
raise pytest.UsageError("file not found: " + arg)
723
def matchnodes(self, matching, names):
724
self.trace("matchnodes", matching, names)
725
self.trace.root.indent += 1
726
nodes = self._matchnodes(matching, names)
728
self.trace("matchnodes finished -> ", num, "nodes")
729
self.trace.root.indent -= 1
731
raise NoMatch(matching, names[:1])
734
def _matchnodes(self, matching, names):
735
if not matching or not names:
739
nextnames = names[1:]
741
for node in matching:
742
if isinstance(node, pytest.Item):
744
resultnodes.append(node)
746
assert isinstance(node, pytest.Collector)
747
rep = collect_one_node(node)
751
# TODO: remove parametrized workaround once collection structure contains parametrization
752
if x.name == name or x.name.split("[")[0] == name:
753
resultnodes.extend(self.matchnodes([x], nextnames))
755
# XXX accept IDs that don't have "()" for class instances
756
if not has_matched and len(rep.result) == 1 and x.name == "()":
757
nextnames.insert(0, name)
758
resultnodes.extend(self.matchnodes([x], nextnames))
759
node.ihook.pytest_collectreport(report=rep)
762
def genitems(self, node):
763
self.trace("genitems", node)
764
if isinstance(node, pytest.Item):
765
node.ihook.pytest_itemcollected(item=node)
768
assert isinstance(node, pytest.Collector)
769
rep = collect_one_node(node)
771
for subnode in rep.result:
772
for x in self.genitems(subnode):
774
node.ihook.pytest_collectreport(report=rep)