test_ok2/pytest/plugin/session.py

298 lines
11 KiB
Python
Raw Normal View History

""" 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. """
2010-09-28 22:38:46 +08:00
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("::")