1 """
2 XML Test Runner for PyUnit
3 """
4
5
6
7
8 from __future__ import print_function
9
10 import codecs
11 import os.path
12 import re
13 import sys
14 import time
15 import traceback
16 import unittest
17 try:
18 from cStringIO import StringIO
19 python2 = True
20 except ImportError:
21 from io import StringIO
22 python2 = False
23 import xml.etree.ElementTree as ET
24 from xml.sax.saxutils import escape
25
26
27 -def cdata(cdata_text):
28 return '<![CDATA[\n{}\n]]>'.format(cdata_text)
29
32 """Information about a particular test.
33
34 Used by _XMLTestResult."""
35
37 (self._class, self._method) = test.id().rsplit('.', 1)
38 self._time = time
39 self._error = None
40 self._failure = None
41
42 @staticmethod
44 """Create a _TestInfo instance for a successful test."""
45 return _TestInfo(test, time)
46
47 @staticmethod
49 """Create a _TestInfo instance for a failed test."""
50 info = _TestInfo(test, time)
51 info._failure = failure
52 return info
53
54 @staticmethod
56 """Create a _TestInfo instance for an erroneous test."""
57 info = _TestInfo(test, time)
58 info._error = error
59 return info
60
62 """Create an XML tag with information about this test case.
63
64 """
65 testcase = ET.Element('testcase')
66 testcase.set('classname', self._class)
67 testcase.set('name', self._method)
68 testcase.set('time', '%.4f' % self._time)
69 if self._failure is not None:
70 self._print_error(testcase, 'failure', self._failure)
71 if self._error is not None:
72 self._print_error(testcase, 'error', self._error)
73 return testcase
74
76 """Print information about this test case in XML format to the
77 supplied stream.
78
79 """
80 stream.write(ET.tostring(self.xml()))
81
82 - def print_report_text(self, stream):
83
84
85
86
87
88
89 stream.write('[Testcase: ' + self._method + ']')
90 if self._failure is not None:
91 stream.write(' ... FAILURE!\n')
92 self._print_error_text(stream, 'failure', self._failure)
93 if self._error is not None:
94 stream.write(' ... ERROR!\n')
95 self._print_error_text(stream, 'error', self._error)
96 if self._failure is None and self._error is None:
97 stream.write(' ... ok\n')
98
100 """
101 Append an XML tag with information from a failure or error to the
102 supplied testcase.
103 """
104 tag = ET.SubElement(testcase, tagname)
105 tag.set('type', str(error[0].__name__))
106 tb_stream = StringIO()
107 traceback.print_tb(error[2], None, tb_stream)
108 tag.text = '%s\n%s' % (str(error[1]), tb_stream.getvalue())
109
110 - def _print_error_text(self, stream, tagname, error):
111 """Print information from a failure or error to the supplied stream."""
112 text = escape(str(error[1]))
113 stream.write('%s: %s\n' % (tagname.upper(), text))
114 tb_stream = StringIO()
115 traceback.print_tb(error[2], None, tb_stream)
116 stream.write(escape(tb_stream.getvalue()))
117 stream.write('-' * 80 + '\n')
118
121 """A test result class that stores result as XML.
122
123 Used by XMLTestRunner."""
124
126 unittest.TestResult.__init__(self)
127 self._test_name = classname
128 self._start_time = None
129 self._tests = []
130 self._error = None
131 self._failure = None
132
134 unittest.TestResult.startTest(self, test)
135 self._error = None
136 self._failure = None
137 self._start_time = time.time()
138
140 time_taken = time.time() - self._start_time
141 unittest.TestResult.stopTest(self, test)
142 if self._error:
143 info = _TestInfo.create_error(test, time_taken, self._error)
144 elif self._failure:
145 info = _TestInfo.create_failure(test, time_taken, self._failure)
146 else:
147 info = _TestInfo.create_success(test, time_taken)
148 self._tests.append(info)
149
151 unittest.TestResult.addError(self, test, err)
152 self._error = err
153
155 unittest.TestResult.addFailure(self, test, err)
156 self._failure = err
157
159 pattern = r'[^\x09\x0A\x0D\x20-\x7E\x85\xA0-\xFF\u0100-\uD7FF\uE000-\uFDCF\uFDE0-\uFFFD]'
160 if python2:
161 pattern = pattern.decode('unicode_escape')
162 else:
163 pattern = codecs.decode(pattern, 'unicode_escape')
164 invalid_chars = re.compile(pattern)
165
166 def invalid_char_replacer(m):
167 return '&#x' + ('%04X' % ord(m.group(0))) + ';'
168 return re.sub(invalid_chars, invalid_char_replacer, str(text))
169
170 - def xml(self, time_taken, out, err):
171 """
172 @return XML tag representing the object
173 @rtype: xml.etree.ElementTree.Element
174 """
175 test_suite = ET.Element('testsuite')
176 test_suite.set('errors', str(len(self.errors)))
177 test_suite.set('failures', str(len(self.failures)))
178 test_suite.set('name', self._test_name)
179 test_suite.set('tests', str(self.testsRun))
180 test_suite.set('time', '%.3f' % time_taken)
181 for info in self._tests:
182 test_suite.append(info.xml())
183 system_out = ET.SubElement(test_suite, 'system-out')
184 system_out.text = cdata(self.filter_nonprintable_text(out))
185 system_err = ET.SubElement(test_suite, 'system-err')
186 system_err.text = cdata(self.filter_nonprintable_text(err))
187 return ET.ElementTree(test_suite)
188
190 """Prints the XML report to the supplied stream.
191
192 The time the tests took to perform as well as the captured standard
193 output and standard error streams must be passed in.a
194
195 """
196 root = self.xml(time_taken, out, err).getroot()
197 stream.write(ET.tostring(root, encoding='utf-8', method='xml').decode('utf-8'))
198
199 - def print_report_text(self, stream, time_taken, out, err):
200 """Prints the text report to the supplied stream.
201
202 The time the tests took to perform as well as the captured standard
203 output and standard error streams must be passed in.a
204
205 """
206
207
208
209
210
211
212
213
214 for info in self._tests:
215 info.print_report_text(stream)
216
219 """A test runner that stores results in XML format compatible with JUnit.
220
221 XMLTestRunner(stream=None) -> XML test runner
222
223 The XML file is written to the supplied stream. If stream is None, the
224 results are stored in a file called TEST-<module>.<class>.xml in the
225 current working directory (if not overridden with the path property),
226 where <module> and <class> are the module and class name of the test class."""
227
229 self._stream = stream
230 self._path = '.'
231
232 - def run(self, test):
233 """Run the given test case or test suite."""
234 class_ = test.__class__
235 classname = class_.__module__ + '.' + class_.__name__
236 if self._stream is None:
237 filename = 'TEST-%s.xml' % classname
238 stream = open(os.path.join(self._path, filename), 'w')
239 stream.write('<?xml version="1.0" encoding="utf-8"?>\n')
240 else:
241 stream = self._stream
242
243 result = _XMLTestResult(classname)
244 start_time = time.time()
245
246
247 old_stdout = sys.stdout
248 old_stderr = sys.stderr
249 sys.stdout = StringIO()
250 sys.stderr = StringIO()
251
252 try:
253 test(result)
254 try:
255 out_s = sys.stdout.getvalue()
256 except AttributeError:
257 out_s = ''
258 try:
259 err_s = sys.stderr.getvalue()
260 except AttributeError:
261 err_s = ''
262 finally:
263 sys.stdout = old_stdout
264 sys.stderr = old_stderr
265
266 time_taken = time.time() - start_time
267 result.print_report(stream, time_taken, out_s, err_s)
268 stream.flush()
269
270 result.print_report_text(sys.stdout, time_taken, out_s, err_s)
271
272 return result
273
276
277 path = property(
278 lambda self: self._path, _set_path, None,
279 """The path where the XML files are stored.
280
281 This property is ignored when the XML file is written to a file
282 stream.""")
283