2009-05-19 05:26:16 +08:00
|
|
|
"""resultlog plugin for machine-readable logging of test results.
|
|
|
|
Useful for buildbot integration code.
|
|
|
|
"""
|
|
|
|
|
2009-02-27 18:18:27 +08:00
|
|
|
import py
|
|
|
|
|
2009-05-19 05:26:16 +08:00
|
|
|
def pytest_addoption(parser):
|
|
|
|
group = parser.addgroup("resultlog", "resultlog plugin options")
|
2009-07-15 03:17:13 +08:00
|
|
|
group.addoption('--resultlog', action="store", dest="resultlog", metavar="path", default=None,
|
2009-05-19 05:26:16 +08:00
|
|
|
help="path for machine-readable result log.")
|
|
|
|
|
|
|
|
def pytest_configure(config):
|
|
|
|
resultlog = config.option.resultlog
|
|
|
|
if resultlog:
|
|
|
|
logfile = open(resultlog, 'w', 1) # line buffered
|
|
|
|
config._resultlog = ResultLog(logfile)
|
|
|
|
config.pluginmanager.register(config._resultlog)
|
|
|
|
|
|
|
|
def pytest_unconfigure(config):
|
|
|
|
resultlog = getattr(config, '_resultlog', None)
|
|
|
|
if resultlog:
|
|
|
|
resultlog.logfile.close()
|
2009-05-23 01:56:05 +08:00
|
|
|
del config._resultlog
|
2009-05-19 05:26:16 +08:00
|
|
|
config.pluginmanager.unregister(resultlog)
|
2009-02-27 18:18:27 +08:00
|
|
|
|
|
|
|
def generic_path(item):
|
|
|
|
chain = item.listchain()
|
|
|
|
gpath = [chain[0].name]
|
|
|
|
fspath = chain[0].fspath
|
|
|
|
fspart = False
|
|
|
|
for node in chain[1:]:
|
|
|
|
newfspath = node.fspath
|
|
|
|
if newfspath == fspath:
|
|
|
|
if fspart:
|
|
|
|
gpath.append(':')
|
|
|
|
fspart = False
|
|
|
|
else:
|
|
|
|
gpath.append('.')
|
|
|
|
else:
|
|
|
|
gpath.append('/')
|
|
|
|
fspart = True
|
|
|
|
name = node.name
|
|
|
|
if name[0] in '([':
|
|
|
|
gpath.pop()
|
|
|
|
gpath.append(name)
|
|
|
|
fspath = newfspath
|
|
|
|
return ''.join(gpath)
|
2009-04-01 11:28:24 +08:00
|
|
|
|
2009-02-27 18:18:27 +08:00
|
|
|
class ResultLog(object):
|
|
|
|
def __init__(self, logfile):
|
|
|
|
self.logfile = logfile # preferably line buffered
|
|
|
|
|
2009-04-04 00:26:21 +08:00
|
|
|
def write_log_entry(self, testpath, shortrepr, longrepr):
|
|
|
|
print >>self.logfile, "%s %s" % (shortrepr, testpath)
|
2009-02-27 18:18:27 +08:00
|
|
|
for line in longrepr.splitlines():
|
|
|
|
print >>self.logfile, " %s" % line
|
|
|
|
|
2009-05-23 01:56:05 +08:00
|
|
|
def log_outcome(self, node, shortrepr, longrepr):
|
|
|
|
testpath = generic_path(node)
|
2009-04-04 00:26:21 +08:00
|
|
|
self.write_log_entry(testpath, shortrepr, longrepr)
|
|
|
|
|
2009-06-09 00:31:10 +08:00
|
|
|
def pytest_runtest_logreport(self, rep):
|
2009-04-06 04:16:27 +08:00
|
|
|
code = rep.shortrepr
|
|
|
|
if rep.passed:
|
2009-04-04 00:26:21 +08:00
|
|
|
longrepr = ""
|
2009-04-06 04:16:27 +08:00
|
|
|
elif rep.failed:
|
|
|
|
longrepr = str(rep.longrepr)
|
|
|
|
elif rep.skipped:
|
|
|
|
longrepr = str(rep.longrepr.reprcrash.message)
|
2009-05-23 01:56:05 +08:00
|
|
|
self.log_outcome(rep.item, code, longrepr)
|
2009-04-04 00:26:21 +08:00
|
|
|
|
2009-04-09 07:33:48 +08:00
|
|
|
def pytest_collectreport(self, rep):
|
2009-04-06 04:16:27 +08:00
|
|
|
if not rep.passed:
|
|
|
|
if rep.failed:
|
2009-04-04 00:26:21 +08:00
|
|
|
code = "F"
|
|
|
|
else:
|
2009-04-06 04:16:27 +08:00
|
|
|
assert rep.skipped
|
2009-04-04 00:26:21 +08:00
|
|
|
code = "S"
|
2009-04-06 04:16:27 +08:00
|
|
|
longrepr = str(rep.longrepr.reprcrash)
|
2009-05-23 01:56:05 +08:00
|
|
|
self.log_outcome(rep.collector, code, longrepr)
|
2009-02-27 18:18:27 +08:00
|
|
|
|
2009-04-09 08:12:10 +08:00
|
|
|
def pytest_internalerror(self, excrepr):
|
2009-04-04 00:26:21 +08:00
|
|
|
path = excrepr.reprcrash.path
|
|
|
|
self.write_log_entry(path, '!', str(excrepr))
|
2009-02-27 18:18:27 +08:00
|
|
|
|
2009-04-03 18:57:34 +08:00
|
|
|
|
2009-02-27 18:18:27 +08:00
|
|
|
# ===============================================================================
|
|
|
|
#
|
|
|
|
# plugin tests
|
|
|
|
#
|
|
|
|
# ===============================================================================
|
|
|
|
|
|
|
|
import os, StringIO
|
|
|
|
|
|
|
|
def test_generic_path():
|
|
|
|
from py.__.test.collect import Node, Item, FSCollector
|
2009-06-12 00:23:32 +08:00
|
|
|
p1 = Node('a')
|
2009-02-27 18:18:27 +08:00
|
|
|
assert p1.fspath is None
|
|
|
|
p2 = Node('B', parent=p1)
|
|
|
|
p3 = Node('()', parent = p2)
|
|
|
|
item = Item('c', parent = p3)
|
|
|
|
|
|
|
|
res = generic_path(item)
|
|
|
|
assert res == 'a.B().c'
|
|
|
|
|
2009-06-12 00:23:32 +08:00
|
|
|
p0 = FSCollector('proj/test')
|
2009-02-27 18:18:27 +08:00
|
|
|
p1 = FSCollector('proj/test/a', parent=p0)
|
|
|
|
p2 = Node('B', parent=p1)
|
|
|
|
p3 = Node('()', parent = p2)
|
|
|
|
p4 = Node('c', parent=p3)
|
|
|
|
item = Item('[1]', parent = p4)
|
|
|
|
|
|
|
|
res = generic_path(item)
|
|
|
|
assert res == 'test/a:B().c[1]'
|
|
|
|
|
|
|
|
def test_write_log_entry():
|
|
|
|
reslog = ResultLog(None)
|
|
|
|
reslog.logfile = StringIO.StringIO()
|
2009-04-04 00:26:21 +08:00
|
|
|
reslog.write_log_entry('name', '.', '')
|
2009-02-27 18:18:27 +08:00
|
|
|
entry = reslog.logfile.getvalue()
|
|
|
|
assert entry[-1] == '\n'
|
|
|
|
entry_lines = entry.splitlines()
|
|
|
|
assert len(entry_lines) == 1
|
|
|
|
assert entry_lines[0] == '. name'
|
|
|
|
|
|
|
|
reslog.logfile = StringIO.StringIO()
|
2009-04-04 00:26:21 +08:00
|
|
|
reslog.write_log_entry('name', 's', 'Skipped')
|
2009-02-27 18:18:27 +08:00
|
|
|
entry = reslog.logfile.getvalue()
|
|
|
|
assert entry[-1] == '\n'
|
|
|
|
entry_lines = entry.splitlines()
|
|
|
|
assert len(entry_lines) == 2
|
|
|
|
assert entry_lines[0] == 's name'
|
|
|
|
assert entry_lines[1] == ' Skipped'
|
|
|
|
|
|
|
|
reslog.logfile = StringIO.StringIO()
|
2009-04-04 00:26:21 +08:00
|
|
|
reslog.write_log_entry('name', 's', 'Skipped\n')
|
2009-02-27 18:18:27 +08:00
|
|
|
entry = reslog.logfile.getvalue()
|
|
|
|
assert entry[-1] == '\n'
|
|
|
|
entry_lines = entry.splitlines()
|
|
|
|
assert len(entry_lines) == 2
|
|
|
|
assert entry_lines[0] == 's name'
|
|
|
|
assert entry_lines[1] == ' Skipped'
|
|
|
|
|
|
|
|
reslog.logfile = StringIO.StringIO()
|
|
|
|
longrepr = ' tb1\n tb 2\nE tb3\nSome Error'
|
2009-04-04 00:26:21 +08:00
|
|
|
reslog.write_log_entry('name', 'F', longrepr)
|
2009-02-27 18:18:27 +08:00
|
|
|
entry = reslog.logfile.getvalue()
|
|
|
|
assert entry[-1] == '\n'
|
|
|
|
entry_lines = entry.splitlines()
|
|
|
|
assert len(entry_lines) == 5
|
|
|
|
assert entry_lines[0] == 'F name'
|
|
|
|
assert entry_lines[1:] == [' '+line for line in longrepr.splitlines()]
|
|
|
|
|
|
|
|
|
|
|
|
class TestWithFunctionIntegration:
|
|
|
|
# XXX (hpk) i think that the resultlog plugin should
|
|
|
|
# provide a Parser object so that one can remain
|
|
|
|
# ignorant regarding formatting details.
|
|
|
|
def getresultlog(self, testdir, arg):
|
|
|
|
resultlog = testdir.tmpdir.join("resultlog")
|
2009-04-15 00:30:26 +08:00
|
|
|
testdir.plugins.append("resultlog")
|
2009-02-27 18:18:27 +08:00
|
|
|
args = ["--resultlog=%s" % resultlog] + [arg]
|
|
|
|
testdir.runpytest(*args)
|
|
|
|
return filter(None, resultlog.readlines(cr=0))
|
|
|
|
|
2009-05-23 05:50:35 +08:00
|
|
|
def test_collection_report(self, testdir):
|
2009-02-27 18:18:27 +08:00
|
|
|
ok = testdir.makepyfile(test_collection_ok="")
|
|
|
|
skip = testdir.makepyfile(test_collection_skip="import py ; py.test.skip('hello')")
|
|
|
|
fail = testdir.makepyfile(test_collection_fail="XXX")
|
|
|
|
lines = self.getresultlog(testdir, ok)
|
|
|
|
assert not lines
|
|
|
|
|
|
|
|
lines = self.getresultlog(testdir, skip)
|
|
|
|
assert len(lines) == 2
|
|
|
|
assert lines[0].startswith("S ")
|
|
|
|
assert lines[0].endswith("test_collection_skip.py")
|
|
|
|
assert lines[1].startswith(" ")
|
|
|
|
assert lines[1].endswith("test_collection_skip.py:1: Skipped: 'hello'")
|
|
|
|
|
|
|
|
lines = self.getresultlog(testdir, fail)
|
|
|
|
assert lines
|
|
|
|
assert lines[0].startswith("F ")
|
|
|
|
assert lines[0].endswith("test_collection_fail.py"), lines[0]
|
|
|
|
for x in lines[1:]:
|
|
|
|
assert x.startswith(" ")
|
|
|
|
assert "XXX" in "".join(lines[1:])
|
|
|
|
|
2009-05-23 05:50:35 +08:00
|
|
|
def test_log_test_outcomes(self, testdir):
|
2009-02-27 18:18:27 +08:00
|
|
|
mod = testdir.makepyfile(test_mod="""
|
|
|
|
import py
|
|
|
|
def test_pass(): pass
|
|
|
|
def test_skip(): py.test.skip("hello")
|
|
|
|
def test_fail(): raise ValueError("val")
|
|
|
|
""")
|
|
|
|
lines = self.getresultlog(testdir, mod)
|
|
|
|
assert len(lines) >= 3
|
|
|
|
assert lines[0].startswith(". ")
|
|
|
|
assert lines[0].endswith("test_pass")
|
|
|
|
assert lines[1].startswith("s "), lines[1]
|
|
|
|
assert lines[1].endswith("test_skip")
|
|
|
|
assert lines[2].find("hello") != -1
|
|
|
|
|
|
|
|
assert lines[3].startswith("F ")
|
|
|
|
assert lines[3].endswith("test_fail")
|
|
|
|
tb = "".join(lines[4:])
|
|
|
|
assert tb.find("ValueError") != -1
|
|
|
|
|
|
|
|
def test_internal_exception(self):
|
|
|
|
# they are produced for example by a teardown failing
|
|
|
|
# at the end of the run
|
|
|
|
try:
|
|
|
|
raise ValueError
|
|
|
|
except ValueError:
|
2009-04-03 22:18:47 +08:00
|
|
|
excinfo = py.code.ExceptionInfo()
|
2009-02-27 18:18:27 +08:00
|
|
|
reslog = ResultLog(StringIO.StringIO())
|
2009-04-09 08:12:10 +08:00
|
|
|
reslog.pytest_internalerror(excinfo.getrepr())
|
2009-02-27 18:18:27 +08:00
|
|
|
entry = reslog.logfile.getvalue()
|
|
|
|
entry_lines = entry.splitlines()
|
|
|
|
|
|
|
|
assert entry_lines[0].startswith('! ')
|
|
|
|
assert os.path.basename(__file__)[:-1] in entry_lines[0] #.py/.pyc
|
|
|
|
assert entry_lines[-1][0] == ' '
|
|
|
|
assert 'ValueError' in entry
|
|
|
|
|
2009-05-23 05:50:35 +08:00
|
|
|
def test_generic(testdir, LineMatcher):
|
2009-04-15 00:30:26 +08:00
|
|
|
testdir.plugins.append("resultlog")
|
2009-02-27 18:18:27 +08:00
|
|
|
testdir.makepyfile("""
|
|
|
|
import py
|
|
|
|
def test_pass():
|
|
|
|
pass
|
|
|
|
def test_fail():
|
|
|
|
assert 0
|
|
|
|
def test_skip():
|
|
|
|
py.test.skip("")
|
|
|
|
""")
|
|
|
|
testdir.runpytest("--resultlog=result.log")
|
|
|
|
lines = testdir.tmpdir.join("result.log").readlines(cr=0)
|
|
|
|
LineMatcher(lines).fnmatch_lines([
|
|
|
|
". *:test_pass",
|
|
|
|
"F *:test_fail",
|
|
|
|
"s *:test_skip",
|
|
|
|
])
|
|
|
|
|