1 # Copyright 2015 gRPC authors.
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
15 from __future__ import absolute_import
21 from xml.etree import ElementTree
26 from tests import _loader
30 collections.namedtuple('CaseResult', [
31 'id', 'name', 'kind', 'stdout', 'stderr', 'skip_reason', 'traceback'
33 """A serializable result of a single test case.
36 id (object): Any serializable object used to denote the identity of this
38 name (str or None): A human-readable name of the test case.
39 kind (CaseResult.Kind): The kind of test result.
40 stdout (object or None): Output on stdout, or None if nothing was captured.
41 stderr (object or None): Output on stderr, or None if nothing was captured.
42 skip_reason (object or None): The reason the test was skipped. Must be
43 something if self.kind is CaseResult.Kind.SKIP, else None.
44 traceback (object or None): The traceback of the test. Must be something if
45 self.kind is CaseResult.Kind.{ERROR, FAILURE, EXPECTED_FAILURE}, else
56 EXPECTED_FAILURE = 'expected failure'
57 UNEXPECTED_SUCCESS = 'unexpected success'
67 """Helper keyword constructor for the namedtuple.
69 See this class' attributes for information on the arguments."""
71 assert name is None or isinstance(name, str)
72 if kind is CaseResult.Kind.UNTESTED:
74 elif kind is CaseResult.Kind.RUNNING:
76 elif kind is CaseResult.Kind.ERROR:
77 assert traceback is not None
78 elif kind is CaseResult.Kind.FAILURE:
79 assert traceback is not None
80 elif kind is CaseResult.Kind.SUCCESS:
82 elif kind is CaseResult.Kind.SKIP:
83 assert skip_reason is not None
84 elif kind is CaseResult.Kind.EXPECTED_FAILURE:
85 assert traceback is not None
86 elif kind is CaseResult.Kind.UNEXPECTED_SUCCESS:
90 return super(cls, CaseResult).__new__(cls, id, name, kind, stdout,
91 stderr, skip_reason, traceback)
100 """Get a new validated CaseResult with the fields updated.
102 See this class' attributes for information on the arguments."""
103 name = self.name if name is None else name
104 kind = self.kind if kind is None else kind
105 stdout = self.stdout if stdout is None else stdout
106 stderr = self.stderr if stderr is None else stderr
107 skip_reason = self.skip_reason if skip_reason is None else skip_reason
108 traceback = self.traceback if traceback is None else traceback
109 return CaseResult(id=self.id,
114 skip_reason=skip_reason,
118 class AugmentedResult(unittest.TestResult):
119 """unittest.Result that keeps track of additional information.
121 Uses CaseResult objects to store test-case results, providing additional
122 information beyond that of the standard Python unittest library, such as
126 id_map (callable): A unary callable mapping unittest.TestCase objects to
128 cases (dict): A dictionary mapping from the identifiers returned by id_map
129 to CaseResult objects corresponding to those IDs.
132 def __init__(self, id_map):
133 """Initialize the object with an identifier mapping.
136 id_map (callable): Corresponds to the attribute `id_map`."""
137 super(AugmentedResult, self).__init__()
141 def startTestRun(self):
142 """See unittest.TestResult.startTestRun."""
143 super(AugmentedResult, self).startTestRun()
146 def startTest(self, test):
147 """See unittest.TestResult.startTest."""
148 super(AugmentedResult, self).startTest(test)
149 case_id = self.id_map(test)
150 self.cases[case_id] = CaseResult(id=case_id,
152 kind=CaseResult.Kind.RUNNING)
154 def addError(self, test, err):
155 """See unittest.TestResult.addError."""
156 super(AugmentedResult, self).addError(test, err)
157 case_id = self.id_map(test)
158 self.cases[case_id] = self.cases[case_id].updated(
159 kind=CaseResult.Kind.ERROR, traceback=err)
161 def addFailure(self, test, err):
162 """See unittest.TestResult.addFailure."""
163 super(AugmentedResult, self).addFailure(test, err)
164 case_id = self.id_map(test)
165 self.cases[case_id] = self.cases[case_id].updated(
166 kind=CaseResult.Kind.FAILURE, traceback=err)
168 def addSuccess(self, test):
169 """See unittest.TestResult.addSuccess."""
170 super(AugmentedResult, self).addSuccess(test)
171 case_id = self.id_map(test)
172 self.cases[case_id] = self.cases[case_id].updated(
173 kind=CaseResult.Kind.SUCCESS)
175 def addSkip(self, test, reason):
176 """See unittest.TestResult.addSkip."""
177 super(AugmentedResult, self).addSkip(test, reason)
178 case_id = self.id_map(test)
179 self.cases[case_id] = self.cases[case_id].updated(
180 kind=CaseResult.Kind.SKIP, skip_reason=reason)
182 def addExpectedFailure(self, test, err):
183 """See unittest.TestResult.addExpectedFailure."""
184 super(AugmentedResult, self).addExpectedFailure(test, err)
185 case_id = self.id_map(test)
186 self.cases[case_id] = self.cases[case_id].updated(
187 kind=CaseResult.Kind.EXPECTED_FAILURE, traceback=err)
189 def addUnexpectedSuccess(self, test):
190 """See unittest.TestResult.addUnexpectedSuccess."""
191 super(AugmentedResult, self).addUnexpectedSuccess(test)
192 case_id = self.id_map(test)
193 self.cases[case_id] = self.cases[case_id].updated(
194 kind=CaseResult.Kind.UNEXPECTED_SUCCESS)
196 def set_output(self, test, stdout, stderr):
197 """Set the output attributes for the CaseResult corresponding to a test.
200 test (unittest.TestCase): The TestCase to set the outputs of.
201 stdout (str): Output from stdout to assign to self.id_map(test).
202 stderr (str): Output from stderr to assign to self.id_map(test).
204 case_id = self.id_map(test)
205 self.cases[case_id] = self.cases[case_id].updated(
206 stdout=stdout.decode(), stderr=stderr.decode())
208 def augmented_results(self, filter):
209 """Convenience method to retrieve filtered case results.
212 filter (callable): A unary predicate to filter over CaseResult objects.
214 return (self.cases[case_id]
215 for case_id in self.cases
216 if filter(self.cases[case_id]))
219 class CoverageResult(AugmentedResult):
220 """Extension to AugmentedResult adding coverage.py support per test.\
223 coverage_context (coverage.Coverage): coverage.py management object.
226 def __init__(self, id_map):
227 """See AugmentedResult.__init__."""
228 super(CoverageResult, self).__init__(id_map=id_map)
229 self.coverage_context = None
231 def startTest(self, test):
232 """See unittest.TestResult.startTest.
234 Additionally initializes and begins code coverage tracking."""
235 super(CoverageResult, self).startTest(test)
236 self.coverage_context = coverage.Coverage(data_suffix=True)
237 self.coverage_context.start()
239 def stopTest(self, test):
240 """See unittest.TestResult.stopTest.
242 Additionally stops and deinitializes code coverage tracking."""
243 super(CoverageResult, self).stopTest(test)
244 self.coverage_context.stop()
245 self.coverage_context.save()
246 self.coverage_context = None
249 class _Colors(object):
250 """Namespaced constants for terminal color magic numbers."""
257 UNDERLINE = '\033[4m'
261 class TerminalResult(CoverageResult):
262 """Extension to CoverageResult adding basic terminal reporting."""
264 def __init__(self, out, id_map):
265 """Initialize the result object.
268 out (file-like): Output file to which terminal-colored live results will
270 id_map (callable): See AugmentedResult.__init__.
272 super(TerminalResult, self).__init__(id_map=id_map)
275 def startTestRun(self):
276 """See unittest.TestResult.startTestRun."""
277 super(TerminalResult, self).startTestRun()
278 self.out.write(_Colors.HEADER + 'Testing gRPC Python...\n' +
281 def stopTestRun(self):
282 """See unittest.TestResult.stopTestRun."""
283 super(TerminalResult, self).stopTestRun()
284 self.out.write(summary(self))
287 def addError(self, test, err):
288 """See unittest.TestResult.addError."""
289 super(TerminalResult, self).addError(test, err)
290 self.out.write(_Colors.FAIL + 'ERROR {}\n'.format(test.id()) +
294 def addFailure(self, test, err):
295 """See unittest.TestResult.addFailure."""
296 super(TerminalResult, self).addFailure(test, err)
297 self.out.write(_Colors.FAIL + 'FAILURE {}\n'.format(test.id()) +
301 def addSuccess(self, test):
302 """See unittest.TestResult.addSuccess."""
303 super(TerminalResult, self).addSuccess(test)
304 self.out.write(_Colors.OK + 'SUCCESS {}\n'.format(test.id()) +
308 def addSkip(self, test, reason):
309 """See unittest.TestResult.addSkip."""
310 super(TerminalResult, self).addSkip(test, reason)
311 self.out.write(_Colors.INFO + 'SKIP {}\n'.format(test.id()) +
315 def addExpectedFailure(self, test, err):
316 """See unittest.TestResult.addExpectedFailure."""
317 super(TerminalResult, self).addExpectedFailure(test, err)
318 self.out.write(_Colors.INFO + 'FAILURE_OK {}\n'.format(test.id()) +
322 def addUnexpectedSuccess(self, test):
323 """See unittest.TestResult.addUnexpectedSuccess."""
324 super(TerminalResult, self).addUnexpectedSuccess(test)
325 self.out.write(_Colors.INFO + 'UNEXPECTED_OK {}\n'.format(test.id()) +
330 def _traceback_string(type, value, trace):
331 """Generate a descriptive string of a Python exception traceback.
334 type (class): The type of the exception.
335 value (Exception): The value of the exception.
336 trace (traceback): Traceback of the exception.
339 str: Formatted exception descriptive string.
341 buffer = moves.cStringIO()
342 traceback.print_exception(type, value, trace, file=buffer)
343 return buffer.getvalue()
347 """A summary string of a result object.
350 result (AugmentedResult): The result object to get the summary of.
353 str: The summary string.
355 assert isinstance(result, AugmentedResult)
357 result.augmented_results(
358 lambda case_result: case_result.kind is CaseResult.Kind.UNTESTED))
360 result.augmented_results(
361 lambda case_result: case_result.kind is CaseResult.Kind.RUNNING))
363 result.augmented_results(
364 lambda case_result: case_result.kind is CaseResult.Kind.FAILURE))
366 result.augmented_results(
367 lambda case_result: case_result.kind is CaseResult.Kind.ERROR))
369 result.augmented_results(
370 lambda case_result: case_result.kind is CaseResult.Kind.SUCCESS))
372 result.augmented_results(
373 lambda case_result: case_result.kind is CaseResult.Kind.SKIP))
374 expected_failures = list(
375 result.augmented_results(lambda case_result: case_result.kind is
376 CaseResult.Kind.EXPECTED_FAILURE))
377 unexpected_successes = list(
378 result.augmented_results(lambda case_result: case_result.kind is
379 CaseResult.Kind.UNEXPECTED_SUCCESS))
380 running_names = [case.name for case in running]
381 finished_count = (len(failures) + len(errors) + len(successes) +
382 len(expected_failures) + len(unexpected_successes))
383 statistics = ('{finished} tests finished:\n'
384 '\t{successful} successful\n'
385 '\t{unsuccessful} unsuccessful\n'
386 '\t{skipped} skipped\n'
387 '\t{expected_fail} expected failures\n'
388 '\t{unexpected_successful} unexpected successes\n'
389 'Interrupted Tests:\n'
390 '\t{interrupted}\n'.format(
391 finished=finished_count,
392 successful=len(successes),
393 unsuccessful=(len(failures) + len(errors)),
395 expected_fail=len(expected_failures),
396 unexpected_successful=len(unexpected_successes),
397 interrupted=str(running_names)))
398 tracebacks = '\n\n'.join([
399 (_Colors.FAIL + '{test_name}' + _Colors.END + '\n' + _Colors.BOLD +
400 'traceback:' + _Colors.END + '\n' + '{traceback}\n' + _Colors.BOLD +
401 'stdout:' + _Colors.END + '\n' + '{stdout}\n' + _Colors.BOLD +
402 'stderr:' + _Colors.END + '\n' + '{stderr}\n').format(
403 test_name=result.name,
404 traceback=_traceback_string(*result.traceback),
405 stdout=result.stdout,
406 stderr=result.stderr)
407 for result in itertools.chain(failures, errors)
409 notes = 'Unexpected successes: {}\n'.format(
410 [result.name for result in unexpected_successes])
411 return statistics + '\nErrors/Failures: \n' + tracebacks + '\n' + notes
414 def jenkins_junit_xml(result):
415 """An XML tree object that when written is recognizable by Jenkins.
418 result (AugmentedResult): The result object to get the junit xml output of.
421 ElementTree.ElementTree: The XML tree.
423 assert isinstance(result, AugmentedResult)
424 root = ElementTree.Element('testsuites')
425 suite = ElementTree.SubElement(root, 'testsuite', {
426 'name': 'Python gRPC tests',
428 for case in result.cases.values():
429 if case.kind is CaseResult.Kind.SUCCESS:
430 ElementTree.SubElement(suite, 'testcase', {
433 elif case.kind in (CaseResult.Kind.ERROR, CaseResult.Kind.FAILURE):
434 case_xml = ElementTree.SubElement(suite, 'testcase', {
437 error_xml = ElementTree.SubElement(case_xml, 'error', {})
438 error_xml.text = ''.format(case.stderr, case.traceback)
439 return ElementTree.ElementTree(element=root)