9
9
def pytest_addoption(parser):
10
10
group = parser.getgroup("terminal reporting")
11
group.addoption('--junitxml', action="store", dest="xmlpath",
11
group.addoption('--junitxml', action="store", dest="xmlpath",
12
12
metavar="path", default=None,
13
13
help="create junit-xml style report file at given path.")
14
group.addoption('--junitprefix', action="store", dest="junitprefix",
15
metavar="str", default=None,
16
help="prepend prefix to classnames in junit-xml output")
15
18
def pytest_configure(config):
16
19
xmlpath = config.option.xmlpath
18
config._xml = LogXML(xmlpath)
21
config._xml = LogXML(xmlpath, config.option.junitprefix)
19
22
config.pluginmanager.register(config._xml)
21
24
def pytest_unconfigure(config):
22
25
xml = getattr(config, '_xml', None)
25
28
config.pluginmanager.unregister(xml)
27
30
class LogXML(object):
28
def __init__(self, logfile):
31
def __init__(self, logfile, prefix):
29
32
self.logfile = logfile
30
34
self.test_logs = []
31
35
self.passed = self.skipped = 0
32
36
self.failed = self.errors = 0
33
37
self._durations = {}
35
39
def _opentestcase(self, report):
37
d = {'time': self._durations.pop(report.item, "0")}
40
if hasattr(report, 'item'):
43
node = report.collector
44
d = {'time': self._durations.pop(node, "0")}
38
45
names = [x.replace(".py", "") for x in node.listnames() if x != "()"]
39
d['classname'] = ".".join(names[:-1])
46
classnames = names[:-1]
48
classnames.insert(0, self.prefix)
49
d['classname'] = ".".join(classnames)
50
d['name'] = py.xml.escape(names[-1])
41
51
attrs = ['%s="%s"' % item for item in sorted(d.items())]
42
52
self.test_logs.append("\n<testcase %s>" % " ".join(attrs))
44
54
def _closetestcase(self):
45
55
self.test_logs.append("</testcase>")
57
def appendlog(self, fmt, *args):
58
args = tuple([py.xml.escape(arg) for arg in args])
59
self.test_logs.append(fmt % args)
47
61
def append_pass(self, report):
49
63
self._opentestcase(report)
52
66
def append_failure(self, report):
53
67
self._opentestcase(report)
54
s = py.xml.escape(str(report.longrepr))
55
68
#msg = str(report.longrepr.reprtraceback.extraline)
56
self.test_logs.append(
57
'<failure message="test failure">%s</failure>' % (s))
69
if "xfail" in report.keywords:
71
'<skipped message="xfail-marked test passes unexpectedly"/>')
74
self.appendlog('<failure message="test failure">%s</failure>',
58
77
self._closetestcase()
61
def _opentestcase_collectfailure(self, report):
62
node = report.collector
64
names = [x.replace(".py", "") for x in node.listnames() if x != "()"]
65
d['classname'] = ".".join(names[:-1])
67
attrs = ['%s="%s"' % item for item in sorted(d.items())]
68
self.test_logs.append("\n<testcase %s>" % " ".join(attrs))
70
79
def append_collect_failure(self, report):
71
self._opentestcase_collectfailure(report)
72
s = py.xml.escape(str(report.longrepr))
80
self._opentestcase(report)
73
81
#msg = str(report.longrepr.reprtraceback.extraline)
74
self.test_logs.append(
75
'<failure message="collection failure">%s</failure>' % (s))
82
self.appendlog('<failure message="collection failure">%s</failure>',
76
84
self._closetestcase()
79
87
def append_collect_skipped(self, report):
80
self._opentestcase_collectfailure(report)
81
s = py.xml.escape(str(report.longrepr))
88
self._opentestcase(report)
82
89
#msg = str(report.longrepr.reprtraceback.extraline)
83
self.test_logs.append(
84
'<skipped message="collection skipped">%s</skipped>' % (s))
90
self.appendlog('<skipped message="collection skipped">%s</skipped>',
85
92
self._closetestcase()
88
95
def append_error(self, report):
89
96
self._opentestcase(report)
90
s = py.xml.escape(str(report.longrepr))
91
self.test_logs.append(
92
'<error message="test setup failure">%s</error>' % s)
97
self.appendlog('<error message="test setup failure">%s</error>',
93
99
self._closetestcase()
96
102
def append_skipped(self, report):
97
103
self._opentestcase(report)
98
self.test_logs.append("<skipped/>")
104
if "xfail" in report.keywords:
106
'<skipped message="expected test failure">%s</skipped>',
107
report.keywords['xfail'])
109
self.appendlog("<skipped/>")
99
110
self._closetestcase()
100
111
self.skipped += 1
109
120
self.append_failure(report)
110
121
elif report.skipped:
111
122
self.append_skipped(report)
113
124
def pytest_runtest_call(self, item, __multicall__):
114
125
start = time.time()
116
127
return __multicall__.execute()
118
129
self._durations[item] = time.time() - start
120
131
def pytest_collectreport(self, report):
121
132
if not report.passed:
122
133
if report.failed:
136
147
self.suite_start_time = time.time()
138
149
def pytest_sessionfinish(self, session, exitstatus, __multicall__):
139
logfile = open(self.logfile, 'w', 1) # line buffered
150
if py.std.sys.version_info[0] < 3:
151
logfile = py.std.codecs.open(self.logfile, 'w', encoding='utf-8')
153
logfile = open(self.logfile, 'w', encoding='utf-8')
140
155
suite_stop_time = time.time()
141
156
suite_time_delta = suite_stop_time - self.suite_start_time
142
157
numtests = self.passed + self.failed
158
logfile.write('<?xml version="1.0" encoding="utf-8"?>')
143
159
logfile.write('<testsuite ')
144
160
logfile.write('name="" ')
145
161
logfile.write('errors="%i" ' % self.errors)
151
167
logfile.writelines(self.test_logs)
152
168
logfile.write('</testsuite>')
154
tw = session.config.pluginmanager.getplugin("terminalreporter")._tw
156
tw.sep("-", "generated xml file: %s" %(self.logfile))
171
def pytest_terminal_summary(self, terminalreporter):
172
tw = terminalreporter._tw
173
terminalreporter.write_sep("-", "generated xml file: %s" %(self.logfile))