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