OLD | NEW |
(Empty) | |
| 1 # Copyright 2015, Google Inc. |
| 2 # All rights reserved. |
| 3 # |
| 4 # Redistribution and use in source and binary forms, with or without |
| 5 # modification, are permitted provided that the following conditions are |
| 6 # met: |
| 7 # |
| 8 # * Redistributions of source code must retain the above copyright |
| 9 # notice, this list of conditions and the following disclaimer. |
| 10 # * Redistributions in binary form must reproduce the above |
| 11 # copyright notice, this list of conditions and the following disclaimer |
| 12 # in the documentation and/or other materials provided with the |
| 13 # distribution. |
| 14 # * Neither the name of Google Inc. nor the names of its |
| 15 # contributors may be used to endorse or promote products derived from |
| 16 # this software without specific prior written permission. |
| 17 # |
| 18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| 19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| 20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| 21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| 22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| 23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| 24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| 25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| 26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 29 |
| 30 import cStringIO as StringIO |
| 31 import collections |
| 32 import itertools |
| 33 import traceback |
| 34 import unittest |
| 35 from xml.etree import ElementTree |
| 36 |
| 37 import coverage |
| 38 |
| 39 from tests import _loader |
| 40 |
| 41 |
| 42 class CaseResult(collections.namedtuple('CaseResult', [ |
| 43 'id', 'name', 'kind', 'stdout', 'stderr', 'skip_reason', 'traceback'])): |
| 44 """A serializable result of a single test case. |
| 45 |
| 46 Attributes: |
| 47 id (object): Any serializable object used to denote the identity of this |
| 48 test case. |
| 49 name (str or None): A human-readable name of the test case. |
| 50 kind (CaseResult.Kind): The kind of test result. |
| 51 stdout (object or None): Output on stdout, or None if nothing was captured. |
| 52 stderr (object or None): Output on stderr, or None if nothing was captured. |
| 53 skip_reason (object or None): The reason the test was skipped. Must be |
| 54 something if self.kind is CaseResult.Kind.SKIP, else None. |
| 55 traceback (object or None): The traceback of the test. Must be something if |
| 56 self.kind is CaseResult.Kind.{ERROR, FAILURE, EXPECTED_FAILURE}, else |
| 57 None. |
| 58 """ |
| 59 |
| 60 class Kind: |
| 61 UNTESTED = 'untested' |
| 62 RUNNING = 'running' |
| 63 ERROR = 'error' |
| 64 FAILURE = 'failure' |
| 65 SUCCESS = 'success' |
| 66 SKIP = 'skip' |
| 67 EXPECTED_FAILURE = 'expected failure' |
| 68 UNEXPECTED_SUCCESS = 'unexpected success' |
| 69 |
| 70 def __new__(cls, id=None, name=None, kind=None, stdout=None, stderr=None, |
| 71 skip_reason=None, traceback=None): |
| 72 """Helper keyword constructor for the namedtuple. |
| 73 |
| 74 See this class' attributes for information on the arguments.""" |
| 75 assert id is not None |
| 76 assert name is None or isinstance(name, str) |
| 77 if kind is CaseResult.Kind.UNTESTED: |
| 78 pass |
| 79 elif kind is CaseResult.Kind.RUNNING: |
| 80 pass |
| 81 elif kind is CaseResult.Kind.ERROR: |
| 82 assert traceback is not None |
| 83 elif kind is CaseResult.Kind.FAILURE: |
| 84 assert traceback is not None |
| 85 elif kind is CaseResult.Kind.SUCCESS: |
| 86 pass |
| 87 elif kind is CaseResult.Kind.SKIP: |
| 88 assert skip_reason is not None |
| 89 elif kind is CaseResult.Kind.EXPECTED_FAILURE: |
| 90 assert traceback is not None |
| 91 elif kind is CaseResult.Kind.UNEXPECTED_SUCCESS: |
| 92 pass |
| 93 else: |
| 94 assert False |
| 95 return super(cls, CaseResult).__new__( |
| 96 cls, id, name, kind, stdout, stderr, skip_reason, traceback) |
| 97 |
| 98 def updated(self, name=None, kind=None, stdout=None, stderr=None, |
| 99 skip_reason=None, traceback=None): |
| 100 """Get a new validated CaseResult with the fields updated. |
| 101 |
| 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, name=name, kind=kind, stdout=stdout, |
| 110 stderr=stderr, skip_reason=skip_reason, |
| 111 traceback=traceback) |
| 112 |
| 113 |
| 114 class AugmentedResult(unittest.TestResult): |
| 115 """unittest.Result that keeps track of additional information. |
| 116 |
| 117 Uses CaseResult objects to store test-case results, providing additional |
| 118 information beyond that of the standard Python unittest library, such as |
| 119 standard output. |
| 120 |
| 121 Attributes: |
| 122 id_map (callable): A unary callable mapping unittest.TestCase objects to |
| 123 unique identifiers. |
| 124 cases (dict): A dictionary mapping from the identifiers returned by id_map |
| 125 to CaseResult objects corresponding to those IDs. |
| 126 """ |
| 127 |
| 128 def __init__(self, id_map): |
| 129 """Initialize the object with an identifier mapping. |
| 130 |
| 131 Arguments: |
| 132 id_map (callable): Corresponds to the attribute `id_map`.""" |
| 133 super(AugmentedResult, self).__init__() |
| 134 self.id_map = id_map |
| 135 self.cases = None |
| 136 |
| 137 def startTestRun(self): |
| 138 """See unittest.TestResult.startTestRun.""" |
| 139 super(AugmentedResult, self).startTestRun() |
| 140 self.cases = dict() |
| 141 |
| 142 def stopTestRun(self): |
| 143 """See unittest.TestResult.stopTestRun.""" |
| 144 super(AugmentedResult, self).stopTestRun() |
| 145 |
| 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( |
| 151 id=case_id, name=test.id(), kind=CaseResult.Kind.RUNNING) |
| 152 |
| 153 def addError(self, test, error): |
| 154 """See unittest.TestResult.addError.""" |
| 155 super(AugmentedResult, self).addError(test, error) |
| 156 case_id = self.id_map(test) |
| 157 self.cases[case_id] = self.cases[case_id].updated( |
| 158 kind=CaseResult.Kind.ERROR, traceback=error) |
| 159 |
| 160 def addFailure(self, test, error): |
| 161 """See unittest.TestResult.addFailure.""" |
| 162 super(AugmentedResult, self).addFailure(test, error) |
| 163 case_id = self.id_map(test) |
| 164 self.cases[case_id] = self.cases[case_id].updated( |
| 165 kind=CaseResult.Kind.FAILURE, traceback=error) |
| 166 |
| 167 def addSuccess(self, test): |
| 168 """See unittest.TestResult.addSuccess.""" |
| 169 super(AugmentedResult, self).addSuccess(test) |
| 170 case_id = self.id_map(test) |
| 171 self.cases[case_id] = self.cases[case_id].updated( |
| 172 kind=CaseResult.Kind.SUCCESS) |
| 173 |
| 174 def addSkip(self, test, reason): |
| 175 """See unittest.TestResult.addSkip.""" |
| 176 super(AugmentedResult, self).addSkip(test, reason) |
| 177 case_id = self.id_map(test) |
| 178 self.cases[case_id] = self.cases[case_id].updated( |
| 179 kind=CaseResult.Kind.SKIP, skip_reason=reason) |
| 180 |
| 181 def addExpectedFailure(self, test, error): |
| 182 """See unittest.TestResult.addExpectedFailure.""" |
| 183 super(AugmentedResult, self).addExpectedFailure(test, error) |
| 184 case_id = self.id_map(test) |
| 185 self.cases[case_id] = self.cases[case_id].updated( |
| 186 kind=CaseResult.Kind.EXPECTED_FAILURE, traceback=error) |
| 187 |
| 188 def addUnexpectedSuccess(self, test): |
| 189 """See unittest.TestResult.addUnexpectedSuccess.""" |
| 190 super(AugmentedResult, self).addUnexpectedSuccess(test) |
| 191 case_id = self.id_map(test) |
| 192 self.cases[case_id] = self.cases[case_id].updated( |
| 193 kind=CaseResult.Kind.UNEXPECTED_SUCCESS) |
| 194 |
| 195 def set_output(self, test, stdout, stderr): |
| 196 """Set the output attributes for the CaseResult corresponding to a test. |
| 197 |
| 198 Args: |
| 199 test (unittest.TestCase): The TestCase to set the outputs of. |
| 200 stdout (str): Output from stdout to assign to self.id_map(test). |
| 201 stderr (str): Output from stderr to assign to self.id_map(test). |
| 202 """ |
| 203 case_id = self.id_map(test) |
| 204 self.cases[case_id] = self.cases[case_id].updated( |
| 205 stdout=stdout, stderr=stderr) |
| 206 |
| 207 def augmented_results(self, filter): |
| 208 """Convenience method to retrieve filtered case results. |
| 209 |
| 210 Args: |
| 211 filter (callable): A unary predicate to filter over CaseResult objects. |
| 212 """ |
| 213 return (self.cases[case_id] for case_id in self.cases |
| 214 if filter(self.cases[case_id])) |
| 215 |
| 216 |
| 217 class CoverageResult(AugmentedResult): |
| 218 """Extension to AugmentedResult adding coverage.py support per test.\ |
| 219 |
| 220 Attributes: |
| 221 coverage_context (coverage.Coverage): coverage.py management object. |
| 222 """ |
| 223 |
| 224 def __init__(self, id_map): |
| 225 """See AugmentedResult.__init__.""" |
| 226 super(CoverageResult, self).__init__(id_map=id_map) |
| 227 self.coverage_context = None |
| 228 |
| 229 def startTest(self, test): |
| 230 """See unittest.TestResult.startTest. |
| 231 |
| 232 Additionally initializes and begins code coverage tracking.""" |
| 233 super(CoverageResult, self).startTest(test) |
| 234 self.coverage_context = coverage.Coverage(data_suffix=True) |
| 235 self.coverage_context.start() |
| 236 |
| 237 def stopTest(self, test): |
| 238 """See unittest.TestResult.stopTest. |
| 239 |
| 240 Additionally stops and deinitializes code coverage tracking.""" |
| 241 super(CoverageResult, self).stopTest(test) |
| 242 self.coverage_context.stop() |
| 243 self.coverage_context.save() |
| 244 self.coverage_context = None |
| 245 |
| 246 def stopTestRun(self): |
| 247 """See unittest.TestResult.stopTestRun.""" |
| 248 super(CoverageResult, self).stopTestRun() |
| 249 # TODO(atash): Dig deeper into why the following line fails to properly |
| 250 # combine coverage data from the Cython plugin. |
| 251 #coverage.Coverage().combine() |
| 252 |
| 253 |
| 254 class _Colors: |
| 255 """Namespaced constants for terminal color magic numbers.""" |
| 256 HEADER = '\033[95m' |
| 257 INFO = '\033[94m' |
| 258 OK = '\033[92m' |
| 259 WARN = '\033[93m' |
| 260 FAIL = '\033[91m' |
| 261 BOLD = '\033[1m' |
| 262 UNDERLINE = '\033[4m' |
| 263 END = '\033[0m' |
| 264 |
| 265 |
| 266 class TerminalResult(CoverageResult): |
| 267 """Extension to CoverageResult adding basic terminal reporting.""" |
| 268 |
| 269 def __init__(self, out, id_map): |
| 270 """Initialize the result object. |
| 271 |
| 272 Args: |
| 273 out (file-like): Output file to which terminal-colored live results will |
| 274 be written. |
| 275 id_map (callable): See AugmentedResult.__init__. |
| 276 """ |
| 277 super(TerminalResult, self).__init__(id_map=id_map) |
| 278 self.out = out |
| 279 |
| 280 def startTestRun(self): |
| 281 """See unittest.TestResult.startTestRun.""" |
| 282 super(TerminalResult, self).startTestRun() |
| 283 self.out.write( |
| 284 _Colors.HEADER + |
| 285 'Testing gRPC Python...\n' + |
| 286 _Colors.END) |
| 287 |
| 288 def stopTestRun(self): |
| 289 """See unittest.TestResult.stopTestRun.""" |
| 290 super(TerminalResult, self).stopTestRun() |
| 291 self.out.write(summary(self)) |
| 292 self.out.flush() |
| 293 |
| 294 def addError(self, test, error): |
| 295 """See unittest.TestResult.addError.""" |
| 296 super(TerminalResult, self).addError(test, error) |
| 297 self.out.write( |
| 298 _Colors.FAIL + |
| 299 'ERROR {}\n'.format(test.id()) + |
| 300 _Colors.END) |
| 301 self.out.flush() |
| 302 |
| 303 def addFailure(self, test, error): |
| 304 """See unittest.TestResult.addFailure.""" |
| 305 super(TerminalResult, self).addFailure(test, error) |
| 306 self.out.write( |
| 307 _Colors.FAIL + |
| 308 'FAILURE {}\n'.format(test.id()) + |
| 309 _Colors.END) |
| 310 self.out.flush() |
| 311 |
| 312 def addSuccess(self, test): |
| 313 """See unittest.TestResult.addSuccess.""" |
| 314 super(TerminalResult, self).addSuccess(test) |
| 315 self.out.write( |
| 316 _Colors.OK + |
| 317 'SUCCESS {}\n'.format(test.id()) + |
| 318 _Colors.END) |
| 319 self.out.flush() |
| 320 |
| 321 def addSkip(self, test, reason): |
| 322 """See unittest.TestResult.addSkip.""" |
| 323 super(TerminalResult, self).addSkip(test, reason) |
| 324 self.out.write( |
| 325 _Colors.INFO + |
| 326 'SKIP {}\n'.format(test.id()) + |
| 327 _Colors.END) |
| 328 self.out.flush() |
| 329 |
| 330 def addExpectedFailure(self, test, error): |
| 331 """See unittest.TestResult.addExpectedFailure.""" |
| 332 super(TerminalResult, self).addExpectedFailure(test, error) |
| 333 self.out.write( |
| 334 _Colors.INFO + |
| 335 'FAILURE_OK {}\n'.format(test.id()) + |
| 336 _Colors.END) |
| 337 self.out.flush() |
| 338 |
| 339 def addUnexpectedSuccess(self, test): |
| 340 """See unittest.TestResult.addUnexpectedSuccess.""" |
| 341 super(TerminalResult, self).addUnexpectedSuccess(test) |
| 342 self.out.write( |
| 343 _Colors.INFO + |
| 344 'UNEXPECTED_OK {}\n'.format(test.id()) + |
| 345 _Colors.END) |
| 346 self.out.flush() |
| 347 |
| 348 def _traceback_string(type, value, trace): |
| 349 """Generate a descriptive string of a Python exception traceback. |
| 350 |
| 351 Args: |
| 352 type (class): The type of the exception. |
| 353 value (Exception): The value of the exception. |
| 354 trace (traceback): Traceback of the exception. |
| 355 |
| 356 Returns: |
| 357 str: Formatted exception descriptive string. |
| 358 """ |
| 359 buffer = StringIO.StringIO() |
| 360 traceback.print_exception(type, value, trace, file=buffer) |
| 361 return buffer.getvalue() |
| 362 |
| 363 def summary(result): |
| 364 """A summary string of a result object. |
| 365 |
| 366 Args: |
| 367 result (AugmentedResult): The result object to get the summary of. |
| 368 |
| 369 Returns: |
| 370 str: The summary string. |
| 371 """ |
| 372 assert isinstance(result, AugmentedResult) |
| 373 untested = list(result.augmented_results( |
| 374 lambda case_result: case_result.kind is CaseResult.Kind.UNTESTED)) |
| 375 running = list(result.augmented_results( |
| 376 lambda case_result: case_result.kind is CaseResult.Kind.RUNNING)) |
| 377 failures = list(result.augmented_results( |
| 378 lambda case_result: case_result.kind is CaseResult.Kind.FAILURE)) |
| 379 errors = list(result.augmented_results( |
| 380 lambda case_result: case_result.kind is CaseResult.Kind.ERROR)) |
| 381 successes = list(result.augmented_results( |
| 382 lambda case_result: case_result.kind is CaseResult.Kind.SUCCESS)) |
| 383 skips = list(result.augmented_results( |
| 384 lambda case_result: case_result.kind is CaseResult.Kind.SKIP)) |
| 385 expected_failures = list(result.augmented_results( |
| 386 lambda case_result: case_result.kind is CaseResult.Kind.EXPECTED_FAILURE)) |
| 387 unexpected_successes = list(result.augmented_results( |
| 388 lambda case_result: case_result.kind is CaseResult.Kind.UNEXPECTED_SUCCESS
)) |
| 389 running_names = [case.name for case in running] |
| 390 finished_count = (len(failures) + len(errors) + len(successes) + |
| 391 len(expected_failures) + len(unexpected_successes)) |
| 392 statistics = ( |
| 393 '{finished} tests finished:\n' |
| 394 '\t{successful} successful\n' |
| 395 '\t{unsuccessful} unsuccessful\n' |
| 396 '\t{skipped} skipped\n' |
| 397 '\t{expected_fail} expected failures\n' |
| 398 '\t{unexpected_successful} unexpected successes\n' |
| 399 'Interrupted Tests:\n' |
| 400 '\t{interrupted}\n' |
| 401 .format(finished=finished_count, |
| 402 successful=len(successes), |
| 403 unsuccessful=(len(failures)+len(errors)), |
| 404 skipped=len(skips), |
| 405 expected_fail=len(expected_failures), |
| 406 unexpected_successful=len(unexpected_successes), |
| 407 interrupted=str(running_names))) |
| 408 tracebacks = '\n\n'.join([ |
| 409 (_Colors.FAIL + '{test_name}' + _Colors.END + '\n' + |
| 410 _Colors.BOLD + 'traceback:' + _Colors.END + '\n' + |
| 411 '{traceback}\n' + |
| 412 _Colors.BOLD + 'stdout:' + _Colors.END + '\n' + |
| 413 '{stdout}\n' + |
| 414 _Colors.BOLD + 'stderr:' + _Colors.END + '\n' + |
| 415 '{stderr}\n').format( |
| 416 test_name=result.name, |
| 417 traceback=_traceback_string(*result.traceback), |
| 418 stdout=result.stdout, stderr=result.stderr) |
| 419 for result in itertools.chain(failures, errors) |
| 420 ]) |
| 421 notes = 'Unexpected successes: {}\n'.format([ |
| 422 result.name for result in unexpected_successes]) |
| 423 return statistics + '\nErrors/Failures: \n' + tracebacks + '\n' + notes |
| 424 |
| 425 |
| 426 def jenkins_junit_xml(result): |
| 427 """An XML tree object that when written is recognizable by Jenkins. |
| 428 |
| 429 Args: |
| 430 result (AugmentedResult): The result object to get the junit xml output of. |
| 431 |
| 432 Returns: |
| 433 ElementTree.ElementTree: The XML tree. |
| 434 """ |
| 435 assert isinstance(result, AugmentedResult) |
| 436 root = ElementTree.Element('testsuites') |
| 437 suite = ElementTree.SubElement(root, 'testsuite', { |
| 438 'name': 'Python gRPC tests', |
| 439 }) |
| 440 for case in result.cases.values(): |
| 441 if case.kind is CaseResult.Kind.SUCCESS: |
| 442 ElementTree.SubElement(suite, 'testcase', { |
| 443 'name': case.name, |
| 444 }) |
| 445 elif case.kind in (CaseResult.Kind.ERROR, CaseResult.Kind.FAILURE): |
| 446 case_xml = ElementTree.SubElement(suite, 'testcase', { |
| 447 'name': case.name, |
| 448 }) |
| 449 error_xml = ElementTree.SubElement(case_xml, 'error', {}) |
| 450 error_xml.text = ''.format(case.stderr, case.traceback) |
| 451 return ElementTree.ElementTree(element=root) |
OLD | NEW |