~plane1/maus/devel_624

« back to all changes in this revision

Viewing changes to third_party/nose-0.11.3/lib/python/nose/plugins/xunit.py

  • Committer: tunnell
  • Date: 2010-09-30 13:56:05 UTC
  • Revision ID: tunnell@itchy-20100930135605-wxbkfgy75p0sndk3
add third party

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
 
 
2
"""This plugin provides test results in the standard XUnit XML format.
 
3
 
 
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.
 
7
 
 
8
Add this shell command to your builder ::
 
9
 
 
10
    nosetests --with-xunit
 
11
 
 
12
And by default a file named nosetests.xml will be written to the
 
13
working directory.
 
14
 
 
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::
 
17
 
 
18
    **/nosetests.xml
 
19
 
 
20
If you need to change the name or location of the file, you can set the
 
21
``--xunit-file`` option.
 
22
 
 
23
Here is an abbreviated version of what an XML test report might look like::
 
24
 
 
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):
 
31
            ...
 
32
            TypeError: oops, wrong type
 
33
            </error>
 
34
        </testcase>
 
35
    </testsuite>
 
36
 
 
37
.. _Hudson: https://hudson.dev.java.net/
 
38
 
 
39
"""
 
40
 
 
41
import doctest
 
42
import os
 
43
import traceback
 
44
import re
 
45
import inspect
 
46
from nose.plugins.base import Plugin
 
47
from nose.exc import SkipTest
 
48
from time import time
 
49
from xml.sax import saxutils
 
50
 
 
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]")
 
53
 
 
54
def xml_safe(value):
 
55
    """Replaces invalid XML characters with '?'."""
 
56
    return CONTROL_CHARACTERS.sub('?', value)
 
57
 
 
58
def escape_cdata(cdata):
 
59
    """Escape a string for an XML CDATA section."""
 
60
    return xml_safe(cdata).replace(']]>', ']]>]]&gt;<![CDATA[')
 
61
 
 
62
def nice_classname(obj):
 
63
    """Returns a nice name for class object or class instance.
 
64
 
 
65
        >>> nice_classname(Exception()) # doctest: +ELLIPSIS
 
66
        '...Exception'
 
67
        >>> nice_classname(Exception)
 
68
        'exceptions.Exception'
 
69
 
 
70
    """
 
71
    if inspect.isclass(obj):
 
72
        cls_name = obj.__name__
 
73
    else:
 
74
        cls_name = obj.__class__.__name__
 
75
    mod = inspect.getmodule(obj)
 
76
    if mod:
 
77
        name = mod.__name__
 
78
        # jython
 
79
        if name.startswith('org.python.core.'):
 
80
            name = name[len('org.python.core.'):]
 
81
        return "%s.%s" % (name, cls_name)
 
82
    else:
 
83
        return cls_name
 
84
 
 
85
def exc_message(exc_info):
 
86
    """Return the exception's message."""
 
87
    exc = exc_info[1]
 
88
    if exc is None:
 
89
        # str exception
 
90
        result = exc_info[0]
 
91
    else:
 
92
        try:
 
93
            result = str(exc)
 
94
        except UnicodeEncodeError:
 
95
            try:
 
96
                result = unicode(exc)
 
97
            except UnicodeError:
 
98
                # Fallback to args as neither str nor
 
99
                # unicode(Exception(u'\xe6')) work in Python < 2.6
 
100
                result = exc.args[0]
 
101
    return xml_safe(result)
 
102
 
 
103
class Xunit(Plugin):
 
104
    """This plugin provides test results in the standard XUnit XML format."""
 
105
    name = 'xunit'
 
106
    score = 2000
 
107
    encoding = 'UTF-8'
 
108
    error_report_file = None
 
109
 
 
110
    def _timeTaken(self):
 
111
        if hasattr(self, '_timer'):
 
112
            taken = time() - self._timer
 
113
        else:
 
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
 
117
            taken = 0.0
 
118
        return taken
 
119
 
 
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)
 
126
 
 
127
    def options(self, parser, env):
 
128
        """Sets additional command line options."""
 
129
        Plugin.options(self, parser, env)
 
130
        parser.add_option(
 
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]"))
 
137
 
 
138
    def configure(self, options, config):
 
139
        """Configures the xunit plugin."""
 
140
        Plugin.configure(self, options, config)
 
141
        self.config = config
 
142
        if self.enabled:
 
143
            self.stats = {'errors': 0,
 
144
                          'failures': 0,
 
145
                          'passes': 0,
 
146
                          'skipped': 0
 
147
                          }
 
148
            self.errorlist = []
 
149
            self.error_report_file = open(options.xunit_file, 'w')
 
150
 
 
151
    def report(self, stream):
 
152
        """Writes an Xunit-formatted XML file
 
153
 
 
154
        The file includes a report of test errors and failures.
 
155
 
 
156
        """
 
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)
 
171
 
 
172
    def startTest(self, test):
 
173
        """Initializes a timer before starting a test."""
 
174
        self._timer = time()
 
175
 
 
176
    def addError(self, test, err, capt=None):
 
177
        """Add error output to Xunit report.
 
178
        """
 
179
        taken = self._timeTaken()
 
180
 
 
181
        if issubclass(err[0], SkipTest):
 
182
            type = 'skipped'
 
183
            self.stats['skipped'] += 1
 
184
        else:
 
185
            type = 'error'
 
186
            self.stats['errors'] += 1
 
187
        tb = ''.join(traceback.format_exception(*err))
 
188
        id = test.id()
 
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]),
 
195
             'taken': taken,
 
196
             'type': type,
 
197
             'errtype': self._quoteattr(nice_classname(err[0])),
 
198
             'message': self._quoteattr(exc_message(err)),
 
199
             'tb': escape_cdata(tb),
 
200
             })
 
201
 
 
202
    def addFailure(self, test, err, capt=None, tb_info=None):
 
203
        """Add failure output to Xunit report.
 
204
        """
 
205
        taken = self._timeTaken()
 
206
        tb = ''.join(traceback.format_exception(*err))
 
207
        self.stats['failures'] += 1
 
208
        id = test.id()
 
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]),
 
215
             'taken': taken,
 
216
             'errtype': self._quoteattr(nice_classname(err[0])),
 
217
             'message': self._quoteattr(exc_message(err)),
 
218
             'tb': escape_cdata(tb),
 
219
             })
 
220
 
 
221
    def addSuccess(self, test, capt=None):
 
222
        """Add success output to Xunit report.
 
223
        """
 
224
        taken = self._timeTaken()
 
225
        self.stats['passes'] += 1
 
226
        id = test.id()
 
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]),
 
232
             'taken': taken,
 
233
             })