1# Copyright 2015 gRPC authors. 2# 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 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 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. 14 15from __future__ import absolute_import 16 17import collections 18import itertools 19import traceback 20import unittest 21from xml.etree import ElementTree 22 23import coverage 24from six import moves 25 26from tests import _loader 27 28 29class CaseResult( 30 collections.namedtuple('CaseResult', [ 31 'id', 'name', 'kind', 'stdout', 'stderr', 'skip_reason', 'traceback' 32 ])): 33 """A serializable result of a single test case. 34 35 Attributes: 36 id (object): Any serializable object used to denote the identity of this 37 test case. 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 46 None. 47 """ 48 49 class Kind(object): 50 UNTESTED = 'untested' 51 RUNNING = 'running' 52 ERROR = 'error' 53 FAILURE = 'failure' 54 SUCCESS = 'success' 55 SKIP = 'skip' 56 EXPECTED_FAILURE = 'expected failure' 57 UNEXPECTED_SUCCESS = 'unexpected success' 58 59 def __new__(cls, 60 id=None, 61 name=None, 62 kind=None, 63 stdout=None, 64 stderr=None, 65 skip_reason=None, 66 traceback=None): 67 """Helper keyword constructor for the namedtuple. 68 69 See this class' attributes for information on the arguments.""" 70 assert id is not None 71 assert name is None or isinstance(name, str) 72 if kind is CaseResult.Kind.UNTESTED: 73 pass 74 elif kind is CaseResult.Kind.RUNNING: 75 pass 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: 81 pass 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: 87 pass 88 else: 89 assert False 90 return super(cls, CaseResult).__new__(cls, id, name, kind, stdout, 91 stderr, skip_reason, traceback) 92 93 def updated(self, 94 name=None, 95 kind=None, 96 stdout=None, 97 stderr=None, 98 skip_reason=None, 99 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( 110 id=self.id, 111 name=name, 112 kind=kind, 113 stdout=stdout, 114 stderr=stderr, 115 skip_reason=skip_reason, 116 traceback=traceback) 117 118 119class AugmentedResult(unittest.TestResult): 120 """unittest.Result that keeps track of additional information. 121 122 Uses CaseResult objects to store test-case results, providing additional 123 information beyond that of the standard Python unittest library, such as 124 standard output. 125 126 Attributes: 127 id_map (callable): A unary callable mapping unittest.TestCase objects to 128 unique identifiers. 129 cases (dict): A dictionary mapping from the identifiers returned by id_map 130 to CaseResult objects corresponding to those IDs. 131 """ 132 133 def __init__(self, id_map): 134 """Initialize the object with an identifier mapping. 135 136 Arguments: 137 id_map (callable): Corresponds to the attribute `id_map`.""" 138 super(AugmentedResult, self).__init__() 139 self.id_map = id_map 140 self.cases = None 141 142 def startTestRun(self): 143 """See unittest.TestResult.startTestRun.""" 144 super(AugmentedResult, self).startTestRun() 145 self.cases = dict() 146 147 def startTest(self, test): 148 """See unittest.TestResult.startTest.""" 149 super(AugmentedResult, self).startTest(test) 150 case_id = self.id_map(test) 151 self.cases[case_id] = CaseResult( 152 id=case_id, name=test.id(), kind=CaseResult.Kind.RUNNING) 153 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) 160 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) 167 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) 174 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) 181 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) 188 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) 195 196 def set_output(self, test, stdout, stderr): 197 """Set the output attributes for the CaseResult corresponding to a test. 198 199 Args: 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). 203 """ 204 case_id = self.id_map(test) 205 self.cases[case_id] = self.cases[case_id].updated( 206 stdout=stdout.decode(), stderr=stderr.decode()) 207 208 def augmented_results(self, filter): 209 """Convenience method to retrieve filtered case results. 210 211 Args: 212 filter (callable): A unary predicate to filter over CaseResult objects. 213 """ 214 return (self.cases[case_id] 215 for case_id in self.cases 216 if filter(self.cases[case_id])) 217 218 219class CoverageResult(AugmentedResult): 220 """Extension to AugmentedResult adding coverage.py support per test.\ 221 222 Attributes: 223 coverage_context (coverage.Coverage): coverage.py management object. 224 """ 225 226 def __init__(self, id_map): 227 """See AugmentedResult.__init__.""" 228 super(CoverageResult, self).__init__(id_map=id_map) 229 self.coverage_context = None 230 231 def startTest(self, test): 232 """See unittest.TestResult.startTest. 233 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() 238 239 def stopTest(self, test): 240 """See unittest.TestResult.stopTest. 241 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 247 248 249class _Colors(object): 250 """Namespaced constants for terminal color magic numbers.""" 251 HEADER = '\033[95m' 252 INFO = '\033[94m' 253 OK = '\033[92m' 254 WARN = '\033[93m' 255 FAIL = '\033[91m' 256 BOLD = '\033[1m' 257 UNDERLINE = '\033[4m' 258 END = '\033[0m' 259 260 261class TerminalResult(CoverageResult): 262 """Extension to CoverageResult adding basic terminal reporting.""" 263 264 def __init__(self, out, id_map): 265 """Initialize the result object. 266 267 Args: 268 out (file-like): Output file to which terminal-colored live results will 269 be written. 270 id_map (callable): See AugmentedResult.__init__. 271 """ 272 super(TerminalResult, self).__init__(id_map=id_map) 273 self.out = out 274 275 def startTestRun(self): 276 """See unittest.TestResult.startTestRun.""" 277 super(TerminalResult, self).startTestRun() 278 self.out.write( 279 _Colors.HEADER + 'Testing gRPC Python...\n' + _Colors.END) 280 281 def stopTestRun(self): 282 """See unittest.TestResult.stopTestRun.""" 283 super(TerminalResult, self).stopTestRun() 284 self.out.write(summary(self)) 285 self.out.flush() 286 287 def addError(self, test, err): 288 """See unittest.TestResult.addError.""" 289 super(TerminalResult, self).addError(test, err) 290 self.out.write( 291 _Colors.FAIL + 'ERROR {}\n'.format(test.id()) + _Colors.END) 292 self.out.flush() 293 294 def addFailure(self, test, err): 295 """See unittest.TestResult.addFailure.""" 296 super(TerminalResult, self).addFailure(test, err) 297 self.out.write( 298 _Colors.FAIL + 'FAILURE {}\n'.format(test.id()) + _Colors.END) 299 self.out.flush() 300 301 def addSuccess(self, test): 302 """See unittest.TestResult.addSuccess.""" 303 super(TerminalResult, self).addSuccess(test) 304 self.out.write( 305 _Colors.OK + 'SUCCESS {}\n'.format(test.id()) + _Colors.END) 306 self.out.flush() 307 308 def addSkip(self, test, reason): 309 """See unittest.TestResult.addSkip.""" 310 super(TerminalResult, self).addSkip(test, reason) 311 self.out.write( 312 _Colors.INFO + 'SKIP {}\n'.format(test.id()) + _Colors.END) 313 self.out.flush() 314 315 def addExpectedFailure(self, test, err): 316 """See unittest.TestResult.addExpectedFailure.""" 317 super(TerminalResult, self).addExpectedFailure(test, err) 318 self.out.write( 319 _Colors.INFO + 'FAILURE_OK {}\n'.format(test.id()) + _Colors.END) 320 self.out.flush() 321 322 def addUnexpectedSuccess(self, test): 323 """See unittest.TestResult.addUnexpectedSuccess.""" 324 super(TerminalResult, self).addUnexpectedSuccess(test) 325 self.out.write( 326 _Colors.INFO + 'UNEXPECTED_OK {}\n'.format(test.id()) + _Colors.END) 327 self.out.flush() 328 329 330def _traceback_string(type, value, trace): 331 """Generate a descriptive string of a Python exception traceback. 332 333 Args: 334 type (class): The type of the exception. 335 value (Exception): The value of the exception. 336 trace (traceback): Traceback of the exception. 337 338 Returns: 339 str: Formatted exception descriptive string. 340 """ 341 buffer = moves.cStringIO() 342 traceback.print_exception(type, value, trace, file=buffer) 343 return buffer.getvalue() 344 345 346def summary(result): 347 """A summary string of a result object. 348 349 Args: 350 result (AugmentedResult): The result object to get the summary of. 351 352 Returns: 353 str: The summary string. 354 """ 355 assert isinstance(result, AugmentedResult) 356 untested = list( 357 result.augmented_results( 358 lambda case_result: case_result.kind is CaseResult.Kind.UNTESTED)) 359 running = list( 360 result.augmented_results( 361 lambda case_result: case_result.kind is CaseResult.Kind.RUNNING)) 362 failures = list( 363 result.augmented_results( 364 lambda case_result: case_result.kind is CaseResult.Kind.FAILURE)) 365 errors = list( 366 result.augmented_results( 367 lambda case_result: case_result.kind is CaseResult.Kind.ERROR)) 368 successes = list( 369 result.augmented_results( 370 lambda case_result: case_result.kind is CaseResult.Kind.SUCCESS)) 371 skips = list( 372 result.augmented_results( 373 lambda case_result: case_result.kind is CaseResult.Kind.SKIP)) 374 expected_failures = list( 375 result.augmented_results( 376 lambda case_result: case_result.kind is CaseResult.Kind.EXPECTED_FAILURE 377 )) 378 unexpected_successes = list( 379 result.augmented_results( 380 lambda case_result: case_result.kind is CaseResult.Kind.UNEXPECTED_SUCCESS 381 )) 382 running_names = [case.name for case in running] 383 finished_count = (len(failures) + len(errors) + len(successes) + 384 len(expected_failures) + len(unexpected_successes)) 385 statistics = ('{finished} tests finished:\n' 386 '\t{successful} successful\n' 387 '\t{unsuccessful} unsuccessful\n' 388 '\t{skipped} skipped\n' 389 '\t{expected_fail} expected failures\n' 390 '\t{unexpected_successful} unexpected successes\n' 391 'Interrupted Tests:\n' 392 '\t{interrupted}\n'.format( 393 finished=finished_count, 394 successful=len(successes), 395 unsuccessful=(len(failures) + len(errors)), 396 skipped=len(skips), 397 expected_fail=len(expected_failures), 398 unexpected_successful=len(unexpected_successes), 399 interrupted=str(running_names))) 400 tracebacks = '\n\n'.join( 401 [(_Colors.FAIL + '{test_name}' + _Colors.END + '\n' + _Colors.BOLD + 402 'traceback:' + _Colors.END + '\n' + '{traceback}\n' + _Colors.BOLD + 403 'stdout:' + _Colors.END + '\n' + '{stdout}\n' + _Colors.BOLD + 404 'stderr:' + _Colors.END + '\n' + '{stderr}\n').format( 405 test_name=result.name, 406 traceback=_traceback_string(*result.traceback), 407 stdout=result.stdout, 408 stderr=result.stderr) 409 for result in itertools.chain(failures, errors)]) 410 notes = 'Unexpected successes: {}\n'.format( 411 [result.name for result in unexpected_successes]) 412 return statistics + '\nErrors/Failures: \n' + tracebacks + '\n' + notes 413 414 415def jenkins_junit_xml(result): 416 """An XML tree object that when written is recognizable by Jenkins. 417 418 Args: 419 result (AugmentedResult): The result object to get the junit xml output of. 420 421 Returns: 422 ElementTree.ElementTree: The XML tree. 423 """ 424 assert isinstance(result, AugmentedResult) 425 root = ElementTree.Element('testsuites') 426 suite = ElementTree.SubElement(root, 'testsuite', { 427 'name': 'Python gRPC tests', 428 }) 429 for case in result.cases.values(): 430 if case.kind is CaseResult.Kind.SUCCESS: 431 ElementTree.SubElement(suite, 'testcase', { 432 'name': case.name, 433 }) 434 elif case.kind in (CaseResult.Kind.ERROR, CaseResult.Kind.FAILURE): 435 case_xml = ElementTree.SubElement(suite, 'testcase', { 436 'name': case.name, 437 }) 438 error_xml = ElementTree.SubElement(case_xml, 'error', {}) 439 error_xml.text = ''.format(case.stderr, case.traceback) 440 return ElementTree.ElementTree(element=root) 441