298 lines
11 KiB
Python
298 lines
11 KiB
Python
|
""" basic test session implementation.
|
||
|
|
||
|
* drives collection of tests
|
||
|
* triggers executions of tests
|
||
|
* produces events used by reporting
|
||
|
"""
|
||
|
|
||
|
import py
|
||
|
import pytest
|
||
|
import os, sys
|
||
|
|
||
|
def pytest_addoption(parser):
|
||
|
group = parser.getgroup("general", "running and selection options")
|
||
|
group._addoption('-x', '--exitfirst', action="store_true", default=False,
|
||
|
dest="exitfirst",
|
||
|
help="exit instantly on first error or failed test."),
|
||
|
group._addoption('--maxfail', metavar="num",
|
||
|
action="store", type="int", dest="maxfail", default=0,
|
||
|
help="exit after first num failures or errors.")
|
||
|
|
||
|
group = parser.getgroup("collect", "collection")
|
||
|
group.addoption('--collectonly',
|
||
|
action="store_true", dest="collectonly",
|
||
|
help="only collect tests, don't execute them."),
|
||
|
group.addoption("--ignore", action="append", metavar="path",
|
||
|
help="ignore path during collection (multi-allowed).")
|
||
|
group.addoption('--confcutdir', dest="confcutdir", default=None,
|
||
|
metavar="dir",
|
||
|
help="only load conftest.py's relative to specified dir.")
|
||
|
|
||
|
group = parser.getgroup("debugconfig",
|
||
|
"test process debugging and configuration")
|
||
|
group.addoption('--basetemp', dest="basetemp", default=None, metavar="dir",
|
||
|
help="base temporary directory for this test run.")
|
||
|
|
||
|
def pytest_configure(config):
|
||
|
# compat
|
||
|
if config.getvalue("exitfirst"):
|
||
|
config.option.maxfail = 1
|
||
|
|
||
|
|
||
|
def pytest_cmdline_main(config):
|
||
|
return Session(config).main()
|
||
|
|
||
|
def pytest_perform_collection(session):
|
||
|
collection = session.collection
|
||
|
assert not hasattr(collection, 'items')
|
||
|
hook = session.config.hook
|
||
|
collection.items = items = collection.perform_collect()
|
||
|
hook.pytest_collection_modifyitems(config=session.config, items=items)
|
||
|
hook.pytest_log_finishcollection(collection=collection)
|
||
|
return True
|
||
|
|
||
|
def pytest_runtest_mainloop(session):
|
||
|
if session.config.option.collectonly:
|
||
|
return True
|
||
|
for item in session.collection.items:
|
||
|
item.config.hook.pytest_runtest_protocol(item=item)
|
||
|
if session.shouldstop:
|
||
|
raise session.Interrupted(session.shouldstop)
|
||
|
return True
|
||
|
|
||
|
def pytest_ignore_collect(path, config):
|
||
|
p = path.dirpath()
|
||
|
ignore_paths = config.getconftest_pathlist("collect_ignore", path=p)
|
||
|
ignore_paths = ignore_paths or []
|
||
|
excludeopt = config.getvalue("ignore")
|
||
|
if excludeopt:
|
||
|
ignore_paths.extend([py.path.local(x) for x in excludeopt])
|
||
|
return path in ignore_paths
|
||
|
|
||
|
def pytest_collect_directory(path, parent):
|
||
|
if not parent.recfilter(path): # by default special ".cvs", ...
|
||
|
# check if cmdline specified this dir or a subdir directly
|
||
|
for arg in parent.collection._argfspaths:
|
||
|
if path == arg or arg.relto(path):
|
||
|
break
|
||
|
else:
|
||
|
return
|
||
|
return parent.Directory(path, parent=parent)
|
||
|
|
||
|
def pytest_report_iteminfo(item):
|
||
|
return item.reportinfo()
|
||
|
|
||
|
|
||
|
# exitcodes for the command line
|
||
|
EXIT_OK = 0
|
||
|
EXIT_TESTSFAILED = 1
|
||
|
EXIT_INTERRUPTED = 2
|
||
|
EXIT_INTERNALERROR = 3
|
||
|
EXIT_NOHOSTS = 4
|
||
|
|
||
|
class Session(object):
|
||
|
nodeid = ""
|
||
|
class Interrupted(KeyboardInterrupt):
|
||
|
""" signals an interrupted test run. """
|
||
|
__module__ = 'builtins' # for py3
|
||
|
|
||
|
def __init__(self, config):
|
||
|
self.config = config
|
||
|
self.config.pluginmanager.register(self, name="session", prepend=True)
|
||
|
self._testsfailed = 0
|
||
|
self.shouldstop = False
|
||
|
self.collection = Collection(config) # XXX move elswehre
|
||
|
|
||
|
def pytest_runtest_logreport(self, report):
|
||
|
if report.failed:
|
||
|
self._testsfailed += 1
|
||
|
maxfail = self.config.getvalue("maxfail")
|
||
|
if maxfail and self._testsfailed >= maxfail:
|
||
|
self.shouldstop = "stopping after %d failures" % (
|
||
|
self._testsfailed)
|
||
|
self.collection.shouldstop = self.shouldstop
|
||
|
pytest_collectreport = pytest_runtest_logreport
|
||
|
|
||
|
def main(self):
|
||
|
""" main loop for running tests. """
|
||
|
self.shouldstop = False
|
||
|
self.exitstatus = EXIT_OK
|
||
|
config = self.config
|
||
|
try:
|
||
|
config.pluginmanager.do_configure(config)
|
||
|
config.hook.pytest_sessionstart(session=self)
|
||
|
config.hook.pytest_perform_collection(session=self)
|
||
|
config.hook.pytest_runtest_mainloop(session=self)
|
||
|
except self.config.Error:
|
||
|
raise
|
||
|
except KeyboardInterrupt:
|
||
|
excinfo = py.code.ExceptionInfo()
|
||
|
self.config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
|
||
|
self.exitstatus = EXIT_INTERRUPTED
|
||
|
except:
|
||
|
excinfo = py.code.ExceptionInfo()
|
||
|
self.config.pluginmanager.notify_exception(excinfo)
|
||
|
self.exitstatus = EXIT_INTERNALERROR
|
||
|
if excinfo.errisinstance(SystemExit):
|
||
|
sys.stderr.write("mainloop: caught Spurious SystemExit!\n")
|
||
|
|
||
|
if not self.exitstatus and self._testsfailed:
|
||
|
self.exitstatus = EXIT_TESTSFAILED
|
||
|
self.config.hook.pytest_sessionfinish(
|
||
|
session=self, exitstatus=self.exitstatus,
|
||
|
)
|
||
|
config.pluginmanager.do_unconfigure(config)
|
||
|
return self.exitstatus
|
||
|
|
||
|
class Collection:
|
||
|
def __init__(self, config):
|
||
|
self.config = config
|
||
|
self.topdir = gettopdir(self.config.args)
|
||
|
self._argfspaths = [py.path.local(decodearg(x)[0])
|
||
|
for x in self.config.args]
|
||
|
x = pytest.collect.Directory(fspath=self.topdir,
|
||
|
config=config, collection=self)
|
||
|
self._topcollector = x.consider_dir(self.topdir)
|
||
|
self._topcollector.parent = None
|
||
|
|
||
|
def _normalizearg(self, arg):
|
||
|
return "::".join(self._parsearg(arg))
|
||
|
|
||
|
def _parsearg(self, arg, base=None):
|
||
|
""" return normalized name list for a command line specified id
|
||
|
which might be of the form x/y/z::name1::name2
|
||
|
and should result into the form x::y::z::name1::name2
|
||
|
"""
|
||
|
if base is None:
|
||
|
base = py.path.local()
|
||
|
parts = str(arg).split("::")
|
||
|
path = base.join(parts[0], abs=True)
|
||
|
if not path.check():
|
||
|
raise self.config.Error("file not found: %s" %(path,))
|
||
|
topdir = self.topdir
|
||
|
if path != topdir and not path.relto(topdir):
|
||
|
raise self.config.Error("path %r is not relative to %r" %
|
||
|
(str(path), str(topdir)))
|
||
|
topparts = path.relto(topdir).split(path.sep)
|
||
|
return topparts + parts[1:]
|
||
|
|
||
|
def getid(self, node):
|
||
|
""" return id for node, relative to topdir. """
|
||
|
path = node.fspath
|
||
|
chain = [x for x in node.listchain() if x.fspath == path]
|
||
|
chain = chain[1:]
|
||
|
names = [x.name for x in chain if x.name != "()"]
|
||
|
relpath = path.relto(self.topdir)
|
||
|
if not relpath:
|
||
|
assert path == self.topdir
|
||
|
path = ''
|
||
|
else:
|
||
|
path = relpath
|
||
|
if os.sep != "/":
|
||
|
path = str(path).replace(os.sep, "/")
|
||
|
names.insert(0, path)
|
||
|
return "::".join(names)
|
||
|
|
||
|
def getbyid(self, id):
|
||
|
""" return one or more nodes matching the id. """
|
||
|
names = [x for x in id.split("::") if x]
|
||
|
if names and '/' in names[0]:
|
||
|
names[:1] = names[0].split("/")
|
||
|
return self._match([self._topcollector], names)
|
||
|
|
||
|
def _match(self, matching, names):
|
||
|
while names:
|
||
|
name = names.pop(0)
|
||
|
l = []
|
||
|
for current in matching:
|
||
|
for x in current._memocollect():
|
||
|
if x.name == name:
|
||
|
l.append(x)
|
||
|
elif x.name == "()":
|
||
|
names.insert(0, name)
|
||
|
l.append(x)
|
||
|
break
|
||
|
if not l:
|
||
|
raise ValueError("no node named %r below %r" %(name, current))
|
||
|
matching = l
|
||
|
return matching
|
||
|
|
||
|
def perform_collect(self):
|
||
|
nodes = []
|
||
|
for arg in self.config.args:
|
||
|
names = self._parsearg(arg)
|
||
|
try:
|
||
|
self.genitems([self._topcollector], names, nodes)
|
||
|
except NoMatch:
|
||
|
raise self.config.Error("can't collect: %s" % (arg,))
|
||
|
return nodes
|
||
|
|
||
|
def genitems(self, matching, names, result):
|
||
|
if not matching:
|
||
|
assert not names
|
||
|
return
|
||
|
if names:
|
||
|
name = names[0]
|
||
|
names = names[1:]
|
||
|
else:
|
||
|
name = None
|
||
|
for node in matching:
|
||
|
if isinstance(node, pytest.collect.Item):
|
||
|
if name is None:
|
||
|
node.ihook.pytest_log_itemcollect(item=node)
|
||
|
result.append(node)
|
||
|
continue
|
||
|
assert isinstance(node, pytest.collect.Collector)
|
||
|
node.ihook.pytest_collectstart(collector=node)
|
||
|
rep = node.ihook.pytest_make_collect_report(collector=node)
|
||
|
#print "matching", rep.result, "against name", name
|
||
|
if rep.passed:
|
||
|
if not name:
|
||
|
self.genitems(rep.result, [], result)
|
||
|
else:
|
||
|
matched = False
|
||
|
for x in rep.result:
|
||
|
try:
|
||
|
if x.name == name or x.fspath.basename == name:
|
||
|
self.genitems([x], names, result)
|
||
|
matched = True
|
||
|
elif x.name == "()": # XXX special Instance() case
|
||
|
self.genitems([x], [name] + names, result)
|
||
|
matched = True
|
||
|
except NoMatch:
|
||
|
pass
|
||
|
if not matched:
|
||
|
node.ihook.pytest_collectreport(report=rep)
|
||
|
raise NoMatch(name)
|
||
|
node.ihook.pytest_collectreport(report=rep)
|
||
|
x = getattr(self, 'shouldstop', None)
|
||
|
if x:
|
||
|
raise Session.Interrupted(x)
|
||
|
|
||
|
class NoMatch(Exception):
|
||
|
""" raised if genitems cannot locate a matching names. """
|
||
|
|
||
|
def gettopdir(args):
|
||
|
""" return the top directory for the given paths.
|
||
|
if the common base dir resides in a python package
|
||
|
parent directory of the root package is returned.
|
||
|
"""
|
||
|
fsargs = [py.path.local(decodearg(arg)[0]) for arg in args]
|
||
|
p = fsargs and fsargs[0] or None
|
||
|
for x in fsargs[1:]:
|
||
|
p = p.common(x)
|
||
|
assert p, "cannot determine common basedir of %s" %(fsargs,)
|
||
|
pkgdir = p.pypkgpath()
|
||
|
if pkgdir is None:
|
||
|
if p.check(file=1):
|
||
|
p = p.dirpath()
|
||
|
return p
|
||
|
else:
|
||
|
return pkgdir.dirpath()
|
||
|
|
||
|
def decodearg(arg):
|
||
|
arg = str(arg)
|
||
|
return arg.split("::")
|
||
|
|
||
|
|