Commits

Philip Jenvey committed 48ba10d

o mark Skipped tests with <skipped> testcase tags and include the Exception
message as the message attribute
o remove the xmlsafe implementation for saxutils.quoteattr
o simplify the exception traceback XML by making it CDATA

Comments (0)

Files changed (5)

functional_tests/doc_tests/test_xunit_plugin/test_skips.rst

 FAILED (SKIP=1, errors=1, failures=1)
 
 >>> open(outfile, 'r').read() # doctest: +ELLIPSIS
-'<?xml version="1.0" encoding="UTF-8"?><testsuite name="nosetests" tests="4" errors="1" failures="1" skip="1"><testcase classname="test_skip" name="test_ok" time="..." /><testcase classname="test_skip" name="test_err" time="..."><error type="exceptions.Exception">.../error></testcase><testcase classname="test_skip" name="test_fail" time="..."><failure type="exceptions.AssertionError">...</failure></testcase></testsuite>'
+'<?xml version="1.0" encoding="UTF-8"?><testsuite name="nosetests" tests="4" errors="1" failures="1" skip="1"><testcase classname="test_skip" name="test_ok" time="..." /><testcase classname="test_skip" name="test_err" time="..."><error type="exceptions.Exception" message="oh no">...</error></testcase><testcase classname="test_skip" name="test_fail" time="..."><failure type="exceptions.AssertionError" message="bye">...</failure></testcase><testcase classname="test_skip" name="test_skip" time="..."><skipped type="nose.plugins.skip.SkipTest" message="not me">...</skipped></testcase></testsuite>'

functional_tests/support/xunit/test_xunit_as_suite.py

+# -*- coding: utf-8 -*-
 import sys
 from nose.exc import SkipTest
 import unittest
     def test_error(self):
         raise TypeError("oops, wrong type")
     
+    def test_non_ascii_error(self):
+        raise Exception(u"日本")
+    
     def test_output(self):
         sys.stdout.write("test-generated output\n")
 

functional_tests/test_xunit.py

+# -*- coding: utf-8 -*-
 import os
 import unittest
 from nose.plugins.xunit import Xunit
         print result
         
         assert '<?xml version="1.0" encoding="UTF-8"?>' in result
-        assert '<testsuite name="nosetests" tests="5" errors="1" failures="1" skip="1">' in result
+        assert '<testsuite name="nosetests" tests="6" errors="2" failures="1" skip="1">' in result
         assert '<testcase classname="test_xunit_as_suite.TestForXunit" name="test_error" time="0">' in result
+        assert '<error type="exceptions.Exception" message="日本">' in result
         assert '</testcase>' in result
         assert '</testsuite>' in result
 

nose/plugins/xunit.py

     <testsuite name="nosetests" tests="1" errors="1" failures="0" skip="0">
         <testcase classname="path_to_test_suite.TestSomething"
                   name="test_it" time="0">
-            <error type="exceptions.TypeError">
+            <error type="exceptions.TypeError" message="oops, wrong type">
             Traceback (most recent call last):
             ...
             TypeError: oops, wrong type
 
 """
 
+import doctest
 import os
 import traceback
 import re
 from nose.plugins.base import Plugin
 from nose.exc import SkipTest
 from time import time
-import doctest
+from xml.sax import saxutils
 
-def xmlsafe(s, encoding="utf-8"):
-    """Used internally to escape XML."""
-    if isinstance(s, unicode):
-        s = s.encode(encoding)
-    s = str(s)
-    for src, rep in [('&', '&amp;', ),
-                     ('<', '&lt;', ),
-                     ('>', '&gt;', ),
-                     ('"', '&quot;', ),
-                     ("'", '&#39;', ),
-                     ]:
-        s = s.replace(src, rep)
-    return s
+def escape_cdata(cdata):
+    """Escape a string for an XML CDATA section."""
+    return cdata.replace(']]>', ']]>]]&gt;<![CDATA[')
 
 def nice_classname(obj):
     """Returns a nice name for class object or class instance.
     else:
         return cls_name
 
+def exc_message(exc_info):
+    """Return the exception's message."""
+    exc = exc_info[1]
+    if exc is None:
+        # str exception
+        return exc_info[0]
+
+    try:
+        return str(exc)
+    except UnicodeEncodeError:
+        try:
+            return unicode(exc)
+        except UnicodeError:
+            # Fallback to args as neither str nor
+            # unicode(Exception(u'\xe6')) work in Python < 2.6
+            return exc.args[0]
+
 class Xunit(Plugin):
     """This plugin provides test results in the standard XUnit XML format."""
     name = 'xunit'
             taken = 0.0
         return taken
 
-    def _xmlsafe(self, s):
-        return xmlsafe(s, encoding=self.encoding)
+    def _quoteattr(self, attr):
+        """Escape an XML attribute. Value can be unicode."""
+        if isinstance(attr, unicode):
+            attr = attr.encode(self.encoding)
+        return saxutils.quoteattr(str(attr))
 
     def options(self, parser, env):
         """Sets additional command line options."""
         taken = self._timeTaken()
 
         if issubclass(err[0], SkipTest):
-            self.stats['skipped'] +=1
-            return
+            type = 'skipped'
+            self.stats['skipped'] += 1
+        else:
+            type = 'error'
+            self.stats['errors'] += 1
         tb = ''.join(traceback.format_exception(*err))
-        self.stats['errors'] += 1
         id = test.id()
         self.errorlist.append(
-            '<testcase classname="%(cls)s" name="%(name)s" time="%(taken)d">'
-            '<error type="%(errtype)s">%(tb)s</error></testcase>' %
-            {'cls': self._xmlsafe('.'.join(id.split('.')[:-1])),
-             'name': self._xmlsafe(id.split('.')[-1]),
-             'errtype': self._xmlsafe(nice_classname(err[0])),
-             'tb': self._xmlsafe(tb),
+            '<testcase classname=%(cls)s name=%(name)s time="%(taken)d">'
+            '<%(type)s type=%(errtype)s message=%(message)s><![CDATA[%(tb)s]]>'
+            '</%(type)s></testcase>' %
+            {'cls': self._quoteattr('.'.join(id.split('.')[:-1])),
+             'name': self._quoteattr(id.split('.')[-1]),
              'taken': taken,
+             'type': type,
+             'errtype': self._quoteattr(nice_classname(err[0])),
+             'message': self._quoteattr(exc_message(err)),
+             'tb': escape_cdata(tb),
              })
 
     def addFailure(self, test, err, capt=None, tb_info=None):
         self.stats['failures'] += 1
         id = test.id()
         self.errorlist.append(
-            '<testcase classname="%(cls)s" name="%(name)s" time="%(taken)d">'
-            '<failure type="%(errtype)s">%(tb)s</failure></testcase>' %
-            {'cls': self._xmlsafe('.'.join(id.split('.')[:-1])),
-             'name': self._xmlsafe(id.split('.')[-1]),
-             'errtype': self._xmlsafe(nice_classname(err[0])),
-             'tb': self._xmlsafe(tb),
+            '<testcase classname=%(cls)s name=%(name)s time="%(taken)d">'
+            '<failure type=%(errtype)s message=%(message)s><![CDATA[%(tb)s]]>'
+            '</failure></testcase>' %
+            {'cls': self._quoteattr('.'.join(id.split('.')[:-1])),
+             'name': self._quoteattr(id.split('.')[-1]),
              'taken': taken,
+             'errtype': self._quoteattr(nice_classname(err[0])),
+             'message': self._quoteattr(exc_message(err)),
+             'tb': escape_cdata(tb),
              })
 
     def addSuccess(self, test, capt=None):
         self.stats['passes'] += 1
         id = test.id()
         self.errorlist.append(
-            '<testcase classname="%(cls)s" name="%(name)s" '
+            '<testcase classname=%(cls)s name=%(name)s '
             'time="%(taken)d" />' %
-            {'cls': self._xmlsafe('.'.join(id.split('.')[:-1])),
-             'name': self._xmlsafe(id.split('.')[-1]),
+            {'cls': self._quoteattr('.'.join(id.split('.')[:-1])),
+             'name': self._quoteattr(id.split('.')[-1]),
              'taken': taken,
              })

unit_tests/test_xunit.py

         self.x = Xunit()
 
     def test_all(self):
-        eq_(self.x._xmlsafe(
+        eq_(self.x._quoteattr(
             '''<baz src="http://foo?f=1&b=2" quote="inix hubris 'maximus'?" />'''),
-            ('&lt;baz src=&quot;http://foo?f=1&amp;b=2&quot; '
-                'quote=&quot;inix hubris &#39;maximus&#39;?&quot; /&gt;'))
-
+            ('"&lt;baz src=&quot;http://foo?f=1&amp;b=2&quot; '
+                'quote=&quot;inix hubris \'maximus\'?&quot; /&gt;"'))
 
     def test_unicode_is_utf8_by_default(self):
-        eq_(self.x._xmlsafe(u'Ivan Krsti\u0107'),
-            'Ivan Krsti\xc4\x87')
+        eq_(self.x._quoteattr(u'Ivan Krsti\u0107'),
+            '"Ivan Krsti\xc4\x87"')
 
 
     def test_unicode_custom_utf16_madness(self):
         self.x.encoding = 'utf-16'
-        utf16 = self.x._xmlsafe(u'Ivan Krsti\u0107')
+        utf16 = self.x._quoteattr(u'Ivan Krsti\u0107')[1:-1]
 
         # to avoid big/little endian bytes, assert that we can put it back:
         eq_(utf16.decode('utf16'), u'Ivan Krsti\u0107')