2
"""This plugin provides test results in Mago 1.0 Compatible XML format.
4
It's needed while transitioning from the previous version of mago until
5
the tests have been converted to mago-ng. The output is simplified compared
6
to mago itself because the changes with the xml file are incompatible with
9
Add this shell command to your builder ::
11
nosetests --with-magoxml
13
And by default a file named mago.xml will be written to the
16
If you need to change the name or location of the file, you can set the
17
``--magoxml-file`` option.
19
Here is an example output
21
<suite name="gedit chains">
22
<class>gedit_chains.GEditChain</class>
24
Tests which verify gedit's save file functionality.
26
<case name="ASCII Test">
27
<method>testChain</method>
28
<description>Test ASCII text saving.</description>
29
<result><time>6.2365219593</time><pass>1</pass></result></case>
39
from nose.plugins.base import Plugin
40
from nose.exc import SkipTest
42
from xml.sax import saxutils
43
from subprocess import Popen, PIPE
45
# Invalid XML characters, control characters 0-31 sans \t, \n and \r
46
CONTROL_CHARACTERS = re.compile(r"[\000-\010\013\014\016-\037]")
47
REPORT_XSL = os.path.join(os.path.dirname(__file__), "magoreport.xsl")
50
"""Replaces invalid XML characters with '?'."""
51
return CONTROL_CHARACTERS.sub('?', value)
53
def escape_cdata(cdata):
54
"""Escape a string for an XML CDATA section."""
55
return xml_safe(cdata).replace(']]>', ']]>]]><![CDATA[')
57
def nice_classname(obj):
58
"""Returns a nice name for class object or class instance.
60
>>> nice_classname(Exception()) # doctest: +ELLIPSIS
62
>>> nice_classname(Exception)
63
'exceptions.Exception'
66
if inspect.isclass(obj):
67
cls_name = obj.__name__
69
cls_name = obj.__class__.__name__
70
mod = inspect.getmodule(obj)
74
if name.startswith('org.python.core.'):
75
name = name[len('org.python.core.'):]
76
return "%s.%s" % (name, cls_name)
80
def exc_message(exc_info):
81
"""Return the exception's message."""
89
except UnicodeEncodeError:
93
# Fallback to args as neither str nor
94
# unicode(Exception(u'\xe6')) work in Python < 2.6
96
return xml_safe(result)
98
class MagoXML(Plugin):
99
"""This plugin provides test results in the standard magoxml XML format."""
102
error_report_file = None
103
error_report_html = None
105
def _timeTaken(self):
106
if hasattr(self, '_timer'):
107
taken = time() - self._timer
109
# test died before it ran (probably error in setup())
110
# or success/failure added before test started probably
111
# due to custom TestResult munging
115
def _quoteattr(self, attr):
116
"""Escape an XML attribute. Value can be unicode."""
117
attr = xml_safe(attr)
118
if isinstance(attr, unicode):
119
attr = attr.encode(self.encoding)
120
return saxutils.quoteattr(attr)
122
def options(self, parser, env):
123
"""Sets additional command line options."""
124
Plugin.options(self, parser, env)
126
'--magoxml-file', action='store',
127
dest='magoxml_file', metavar="FILE",
128
default=env.get('NOSE_MAGOXML_FILE', 'mago_result.xml'),
129
help=("Path to xml file to store the mago report in. "
130
"Default is mago_result.xml in the working directory "
131
"[NOSE_MAGOXML_FILE]"))
134
'--magoxml-html', action='store',
135
dest='magoxml_html', metavar="FILE",
136
default=env.get('NOSE_MAGOXML_HTML'),
137
help=("Path to html file to store the mago report in. If this"
138
"option is not set only the xml report is generated"
139
"[NOSE_MAGOXML_HTML]"))
141
def configure(self, options, config):
142
"""Configures the magoxml plugin."""
143
Plugin.configure(self, options, config)
146
self.stats = {'errors': 0,
152
self.error_report_file = open(options.magoxml_file, 'w')
153
self.error_report_html = options.magoxml_html
155
def report(self, stream):
156
"""Writes an magoxml-formatted XML file
158
The file includes a report of test errors and failures.
161
self.stats['encoding'] = self.encoding
162
self.stats['total'] = (self.stats['errors'] + self.stats['failures']
163
+ self.stats['passes'] + self.stats['skipped'])
164
self.stats['suitename'] = self._suitename
165
self.error_report_file.write(
166
'<?xml version="1.0" encoding="%(encoding)s"?>'
167
'<suite name=%(suitename)s tests="%(total)d" '
168
'errors="%(errors)d" failures="%(failures)d" '
169
'skip="%(skipped)d">' % self.stats)
170
self.error_report_file.write(''.join(self.errorlist))
171
self.error_report_file.write('</suite>')
172
self.error_report_file.close()
173
if self.config.verbosity > 1:
174
stream.writeln("-" * 70)
175
stream.writeln("XML: %s" % self.error_report_file.name)
177
if self.error_report_html:
178
cmd = ["xsltproc", "-o", self.error_report_html, REPORT_XSL,
179
self.error_report_file.name]
183
def startTest(self, test):
184
"""Initializes a timer before starting a test."""
186
self._suitename = self._quoteattr(test.id().split('.')[0])
188
def addError(self, test, err, capt=None):
189
"""Add error output to magoxml report.
191
taken = self._timeTaken()
193
if issubclass(err[0], SkipTest):
195
self.stats['skipped'] += 1
198
self.stats['errors'] += 1
199
tb = ''.join(traceback.format_exception(*err))
201
desc = test.shortDescription()
203
self.errorlist.append(
204
'<case name=%(name)s>'
205
'<method>%(cls)s</method>'
206
'<description>%(desc)s</description>'
207
'<result><stacktrace><![CDATA[%(tb)s]]></stacktrace><message><![CDATA[%(message)s]]></message><time>%(taken).2f</time><pass>0</pass></result></case>' %
208
{'name': self._quoteattr(id.split('.')[-1]),
209
'cls': self._quoteattr('.'.join(id.split('.')[-2:])),
211
'tb': escape_cdata(tb),
212
'message': self._quoteattr(exc_message(err)),
216
def addFailure(self, test, err, capt=None, tb_info=None):
217
"""Add failure output to magoxml report.
219
taken = self._timeTaken()
220
tb = ''.join(traceback.format_exception(*err))
221
self.stats['failures'] += 1
223
desc = test.shortDescription()
225
self.errorlist.append(
226
'<case name=%(name)s>'
227
'<method>%(cls)s</method>'
228
'<description>%(desc)s</description>'
229
'<result><stacktrace><![CDATA[%(tb)s]]></stacktrace><message><![CDATA[%(message)s]]></message><time>%(taken).2f</time><pass>0</pass></result></case>' %
230
{'name': self._quoteattr(id.split('.')[-1]),
231
'cls': self._quoteattr('.'.join(id.split('.')[-2:])),
233
'tb': escape_cdata(tb),
234
'message': self._quoteattr(exc_message(err)),
239
def addSuccess(self, test, capt=None):
240
"""Add success output to magoxml report.
242
taken = self._timeTaken()
243
self.stats['passes'] += 1
245
desc = test.shortDescription()
246
self.errorlist.append(
247
'<case name=%(name)s>'
248
'<method>%(cls)s</method>'
249
'<description>%(desc)s</description>'
250
'<result><time>%(taken).2f</time><pass>1</pass></result></case>' %
251
{'name': self._quoteattr(id.split('.')[-1]),
252
'cls': self._quoteattr('.'.join(id.split('.')[-2:])),