~ubuntu-branches/ubuntu/saucy/mago/saucy

« back to all changes in this revision

Viewing changes to mago/noseplugins/magoxml.py

  • Committer: Bazaar Package Importer
  • Author(s): Michael Vogt
  • Date: 2011-02-08 13:32:13 UTC
  • mfrom: (1.1.3 upstream)
  • Revision ID: james.westby@ubuntu.com-20110208133213-m1og7ey0m990chg6
Tags: 0.3+bzr20-0ubuntu1
* debian/rules:
  - updated to debhelper 7
  - use dh_python2 instead of python-central
* debian/pycompat:
  - removed, no longer needed
* debian/control:
  - dropped cdbs and python-central dependencies
* bzr snapshot of the current trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
 
 
2
"""This plugin provides test results in Mago 1.0 Compatible XML format.
 
3
 
 
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
 
7
mago-ng
 
8
 
 
9
Add this shell command to your builder ::
 
10
 
 
11
    nosetests --with-magoxml
 
12
 
 
13
And by default a file named mago.xml will be written to the
 
14
working directory.
 
15
 
 
16
If you need to change the name or location of the file, you can set the
 
17
``--magoxml-file`` option.
 
18
 
 
19
Here is an example output
 
20
 
 
21
<suite name="gedit chains">
 
22
  <class>gedit_chains.GEditChain</class>
 
23
  <description>
 
24
    Tests which verify gedit's save file functionality.
 
25
  </description>
 
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>
 
30
</suite>
 
31
 
 
32
"""
 
33
 
 
34
import doctest
 
35
import os
 
36
import traceback
 
37
import re
 
38
import inspect
 
39
from nose.plugins.base import Plugin
 
40
from nose.exc import SkipTest
 
41
from time import time
 
42
from xml.sax import saxutils
 
43
from subprocess import Popen, PIPE
 
44
 
 
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")
 
48
 
 
49
def xml_safe(value):
 
50
    """Replaces invalid XML characters with '?'."""
 
51
    return CONTROL_CHARACTERS.sub('?', value)
 
52
 
 
53
def escape_cdata(cdata):
 
54
    """Escape a string for an XML CDATA section."""
 
55
    return xml_safe(cdata).replace(']]>', ']]>]]&gt;<![CDATA[')
 
56
 
 
57
def nice_classname(obj):
 
58
    """Returns a nice name for class object or class instance.
 
59
 
 
60
        >>> nice_classname(Exception()) # doctest: +ELLIPSIS
 
61
        '...Exception'
 
62
        >>> nice_classname(Exception)
 
63
        'exceptions.Exception'
 
64
 
 
65
    """
 
66
    if inspect.isclass(obj):
 
67
        cls_name = obj.__name__
 
68
    else:
 
69
        cls_name = obj.__class__.__name__
 
70
    mod = inspect.getmodule(obj)
 
71
    if mod:
 
72
        name = mod.__name__
 
73
        # jython
 
74
        if name.startswith('org.python.core.'):
 
75
            name = name[len('org.python.core.'):]
 
76
        return "%s.%s" % (name, cls_name)
 
77
    else:
 
78
        return cls_name
 
79
 
 
80
def exc_message(exc_info):
 
81
    """Return the exception's message."""
 
82
    exc = exc_info[1]
 
83
    if exc is None:
 
84
        # str exception
 
85
        result = exc_info[0]
 
86
    else:
 
87
        try:
 
88
            result = str(exc)
 
89
        except UnicodeEncodeError:
 
90
            try:
 
91
                result = unicode(exc)
 
92
            except UnicodeError:
 
93
                # Fallback to args as neither str nor
 
94
                # unicode(Exception(u'\xe6')) work in Python < 2.6
 
95
                result = exc.args[0]
 
96
    return xml_safe(result)
 
97
 
 
98
class MagoXML(Plugin):
 
99
    """This plugin provides test results in the standard magoxml XML format."""
 
100
    name = 'magoxml'
 
101
    encoding = 'UTF-8'
 
102
    error_report_file = None
 
103
    error_report_html = None
 
104
 
 
105
    def _timeTaken(self):
 
106
        if hasattr(self, '_timer'):
 
107
            taken = time() - self._timer
 
108
        else:
 
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
 
112
            taken = 0.0
 
113
        return taken
 
114
 
 
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)
 
121
 
 
122
    def options(self, parser, env):
 
123
        """Sets additional command line options."""
 
124
        Plugin.options(self, parser, env)
 
125
        parser.add_option(
 
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]"))
 
132
 
 
133
        parser.add_option(
 
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]"))
 
140
 
 
141
    def configure(self, options, config):
 
142
        """Configures the magoxml plugin."""
 
143
        Plugin.configure(self, options, config)
 
144
        self.config = config
 
145
        if self.enabled:
 
146
            self.stats = {'errors': 0,
 
147
                          'failures': 0,
 
148
                          'passes': 0,
 
149
                          'skipped': 0
 
150
                          }
 
151
            self.errorlist = []
 
152
            self.error_report_file = open(options.magoxml_file, 'w')
 
153
            self.error_report_html = options.magoxml_html
 
154
 
 
155
    def report(self, stream):
 
156
        """Writes an magoxml-formatted XML file
 
157
 
 
158
        The file includes a report of test errors and failures.
 
159
 
 
160
        """
 
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)
 
176
 
 
177
        if self.error_report_html:
 
178
            cmd = ["xsltproc", "-o", self.error_report_html, REPORT_XSL,
 
179
                   self.error_report_file.name]
 
180
            p = Popen(cmd)
 
181
 
 
182
 
 
183
    def startTest(self, test):
 
184
        """Initializes a timer before starting a test."""
 
185
        self._timer = time()
 
186
        self._suitename = self._quoteattr(test.id().split('.')[0])
 
187
 
 
188
    def addError(self, test, err, capt=None):
 
189
        """Add error output to magoxml report.
 
190
        """
 
191
        taken = self._timeTaken()
 
192
 
 
193
        if issubclass(err[0], SkipTest):
 
194
            type = 'skipped'
 
195
            self.stats['skipped'] += 1
 
196
        else:
 
197
            type = 'error'
 
198
            self.stats['errors'] += 1
 
199
        tb = ''.join(traceback.format_exception(*err))
 
200
        id = test.id()
 
201
        desc = test.shortDescription()
 
202
 
 
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:])),
 
210
             'desc': desc,
 
211
             'tb': escape_cdata(tb),
 
212
             'message': self._quoteattr(exc_message(err)),
 
213
             'taken': taken,
 
214
             })
 
215
 
 
216
    def addFailure(self, test, err, capt=None, tb_info=None):
 
217
        """Add failure output to magoxml report.
 
218
        """
 
219
        taken = self._timeTaken()
 
220
        tb = ''.join(traceback.format_exception(*err))
 
221
        self.stats['failures'] += 1
 
222
        id = test.id()
 
223
        desc = test.shortDescription()
 
224
 
 
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:])),
 
232
             'desc': desc,
 
233
             'tb': escape_cdata(tb),
 
234
             'message': self._quoteattr(exc_message(err)),
 
235
             'taken': taken,
 
236
             })
 
237
 
 
238
 
 
239
    def addSuccess(self, test, capt=None):
 
240
        """Add success output to magoxml report.
 
241
        """
 
242
        taken = self._timeTaken()
 
243
        self.stats['passes'] += 1
 
244
        id = test.id()
 
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:])),
 
253
             'desc': desc,
 
254
             'taken': taken,
 
255
             })