2
"""This plugin provides test results in the standard XUnit XML format.
4
It was designed for the `Hudson`_ continuous build system but will
5
probably work for anything else that understands an XUnit-formatted XML
6
representation of test results.
8
Add this shell command to your builder ::
10
nosetests --with-xunit
12
And by default a file named nosetests.xml will be written to the
15
In a Hudson builder, tick the box named "Publish JUnit test result report"
16
under the Post-build Actions and enter this value for Test report XMLs::
20
If you need to change the name or location of the file, you can set the
21
``--xunit-file`` option.
23
Here is an abbreviated version of what an XML test report might look like::
25
<?xml version="1.0" encoding="UTF-8"?>
26
<testsuite name="nosetests" tests="1" errors="1" failures="0" skip="0">
27
<testcase classname="path_to_test_suite.TestSomething"
28
name="test_it" time="0">
29
<error type="exceptions.TypeError" message="oops, wrong type">
30
Traceback (most recent call last):
32
TypeError: oops, wrong type
37
.. _Hudson: https://hudson.dev.java.net/
46
from nose.plugins.base import Plugin
47
from nose.exc import SkipTest
49
from xml.sax import saxutils
51
# Invalid XML characters, control characters 0-31 sans \t, \n and \r
52
CONTROL_CHARACTERS = re.compile(r"[\000-\010\013\014\016-\037]")
55
"""Replaces invalid XML characters with '?'."""
56
return CONTROL_CHARACTERS.sub('?', value)
58
def escape_cdata(cdata):
59
"""Escape a string for an XML CDATA section."""
60
return xml_safe(cdata).replace(']]>', ']]>]]><![CDATA[')
62
def nice_classname(obj):
63
"""Returns a nice name for class object or class instance.
65
>>> nice_classname(Exception()) # doctest: +ELLIPSIS
67
>>> nice_classname(Exception)
68
'exceptions.Exception'
71
if inspect.isclass(obj):
72
cls_name = obj.__name__
74
cls_name = obj.__class__.__name__
75
mod = inspect.getmodule(obj)
79
if name.startswith('org.python.core.'):
80
name = name[len('org.python.core.'):]
81
return "%s.%s" % (name, cls_name)
85
def exc_message(exc_info):
86
"""Return the exception's message."""
94
except UnicodeEncodeError:
98
# Fallback to args as neither str nor
99
# unicode(Exception(u'\xe6')) work in Python < 2.6
101
return xml_safe(result)
104
"""This plugin provides test results in the standard XUnit XML format."""
108
error_report_file = None
110
def _timeTaken(self):
111
if hasattr(self, '_timer'):
112
taken = time() - self._timer
114
# test died before it ran (probably error in setup())
115
# or success/failure added before test started probably
116
# due to custom TestResult munging
120
def _quoteattr(self, attr):
121
"""Escape an XML attribute. Value can be unicode."""
122
attr = xml_safe(attr)
123
if isinstance(attr, unicode):
124
attr = attr.encode(self.encoding)
125
return saxutils.quoteattr(attr)
127
def options(self, parser, env):
128
"""Sets additional command line options."""
129
Plugin.options(self, parser, env)
131
'--xunit-file', action='store',
132
dest='xunit_file', metavar="FILE",
133
default=env.get('NOSE_XUNIT_FILE', 'nosetests.xml'),
134
help=("Path to xml file to store the xunit report in. "
135
"Default is nosetests.xml in the working directory "
136
"[NOSE_XUNIT_FILE]"))
138
def configure(self, options, config):
139
"""Configures the xunit plugin."""
140
Plugin.configure(self, options, config)
143
self.stats = {'errors': 0,
149
self.error_report_file = open(options.xunit_file, 'w')
151
def report(self, stream):
152
"""Writes an Xunit-formatted XML file
154
The file includes a report of test errors and failures.
157
self.stats['encoding'] = self.encoding
158
self.stats['total'] = (self.stats['errors'] + self.stats['failures']
159
+ self.stats['passes'] + self.stats['skipped'])
160
self.error_report_file.write(
161
'<?xml version="1.0" encoding="%(encoding)s"?>'
162
'<testsuite name="nosetests" tests="%(total)d" '
163
'errors="%(errors)d" failures="%(failures)d" '
164
'skip="%(skipped)d">' % self.stats)
165
self.error_report_file.write(''.join(self.errorlist))
166
self.error_report_file.write('</testsuite>')
167
self.error_report_file.close()
168
if self.config.verbosity > 1:
169
stream.writeln("-" * 70)
170
stream.writeln("XML: %s" % self.error_report_file.name)
172
def startTest(self, test):
173
"""Initializes a timer before starting a test."""
176
def addError(self, test, err, capt=None):
177
"""Add error output to Xunit report.
179
taken = self._timeTaken()
181
if issubclass(err[0], SkipTest):
183
self.stats['skipped'] += 1
186
self.stats['errors'] += 1
187
tb = ''.join(traceback.format_exception(*err))
189
self.errorlist.append(
190
'<testcase classname=%(cls)s name=%(name)s time="%(taken)d">'
191
'<%(type)s type=%(errtype)s message=%(message)s><![CDATA[%(tb)s]]>'
192
'</%(type)s></testcase>' %
193
{'cls': self._quoteattr('.'.join(id.split('.')[:-1])),
194
'name': self._quoteattr(id.split('.')[-1]),
197
'errtype': self._quoteattr(nice_classname(err[0])),
198
'message': self._quoteattr(exc_message(err)),
199
'tb': escape_cdata(tb),
202
def addFailure(self, test, err, capt=None, tb_info=None):
203
"""Add failure output to Xunit report.
205
taken = self._timeTaken()
206
tb = ''.join(traceback.format_exception(*err))
207
self.stats['failures'] += 1
209
self.errorlist.append(
210
'<testcase classname=%(cls)s name=%(name)s time="%(taken)d">'
211
'<failure type=%(errtype)s message=%(message)s><![CDATA[%(tb)s]]>'
212
'</failure></testcase>' %
213
{'cls': self._quoteattr('.'.join(id.split('.')[:-1])),
214
'name': self._quoteattr(id.split('.')[-1]),
216
'errtype': self._quoteattr(nice_classname(err[0])),
217
'message': self._quoteattr(exc_message(err)),
218
'tb': escape_cdata(tb),
221
def addSuccess(self, test, capt=None):
222
"""Add success output to Xunit report.
224
taken = self._timeTaken()
225
self.stats['passes'] += 1
227
self.errorlist.append(
228
'<testcase classname=%(cls)s name=%(name)s '
229
'time="%(taken)d" />' %
230
{'cls': self._quoteattr('.'.join(id.split('.')[:-1])),
231
'name': self._quoteattr(id.split('.')[-1]),