| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 1 | import traceback, sys |
| 2 | from unittest import TestResult |
| 3 | import datetime |
| 4 | |
| 5 | from tcmessages import TeamcityServiceMessages |
| 6 | |
| 7 | PYTHON_VERSION_MAJOR = sys.version_info[0] |
| 8 | |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 9 | |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 10 | def strclass(cls): |
| 11 | if not cls.__name__: |
| 12 | return cls.__module__ |
| 13 | return "%s.%s" % (cls.__module__, cls.__name__) |
| 14 | |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 15 | |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 16 | def smart_str(s): |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 17 | encoding = 'utf-8' |
| 18 | errors = 'strict' |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 19 | if PYTHON_VERSION_MAJOR < 3: |
| 20 | is_string = isinstance(s, basestring) |
| 21 | else: |
| 22 | is_string = isinstance(s, str) |
| 23 | if not is_string: |
| 24 | try: |
| 25 | return str(s) |
| 26 | except UnicodeEncodeError: |
| 27 | if isinstance(s, Exception): |
| 28 | # An Exception subclass containing non-ASCII data that doesn't |
| 29 | # know how to print itself properly. We shouldn't raise a |
| 30 | # further exception. |
| 31 | return ' '.join([smart_str(arg) for arg in s]) |
| 32 | return unicode(s).encode(encoding, errors) |
| 33 | elif isinstance(s, unicode): |
| 34 | return s.encode(encoding, errors) |
| 35 | else: |
| 36 | return s |
| 37 | |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 38 | |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 39 | class TeamcityTestResult(TestResult): |
| 40 | def __init__(self, stream=sys.stdout, *args, **kwargs): |
| 41 | TestResult.__init__(self) |
| 42 | for arg, value in kwargs.items(): |
| 43 | setattr(self, arg, value) |
| 44 | self.output = stream |
| 45 | self.messages = TeamcityServiceMessages(self.output, prepend_linebreak=True) |
| 46 | self.messages.testMatrixEntered() |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 47 | self.current_failed = False |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 48 | self.current_suite = None |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 49 | self.subtest_suite = None |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 50 | |
| 51 | def find_first(self, val): |
| 52 | quot = val[0] |
| 53 | count = 1 |
| 54 | quote_ind = val[count:].find(quot) |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 55 | while quote_ind != -1 and val[count + quote_ind - 1] == "\\": |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 56 | count = count + quote_ind + 1 |
| 57 | quote_ind = val[count:].find(quot) |
| 58 | |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 59 | return val[0:quote_ind + count + 1] |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 60 | |
| 61 | def find_second(self, val): |
| 62 | val_index = val.find("!=") |
| 63 | if val_index != -1: |
| 64 | count = 1 |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 65 | val = val[val_index + 2:].strip() |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 66 | quot = val[0] |
| 67 | quote_ind = val[count:].find(quot) |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 68 | while quote_ind != -1 and val[count + quote_ind - 1] == "\\": |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 69 | count = count + quote_ind + 1 |
| 70 | quote_ind = val[count:].find(quot) |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 71 | return val[0:quote_ind + count + 1] |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 72 | |
| 73 | else: |
| 74 | quot = val[-1] |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 75 | quote_ind = val[:len(val) - 1].rfind(quot) |
| 76 | while quote_ind != -1 and val[quote_ind - 1] == "\\": |
| 77 | quote_ind = val[:quote_ind - 1].rfind(quot) |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 78 | return val[quote_ind:] |
| 79 | |
| 80 | def formatErr(self, err): |
| 81 | exctype, value, tb = err |
| 82 | return ''.join(traceback.format_exception(exctype, value, tb)) |
| 83 | |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 84 | def getTestName(self, test, is_subtest=False): |
| 85 | if is_subtest: |
| 86 | test_name = self.getTestName(test.test_case) |
| 87 | return "{} {}".format(test_name, test._subDescription()) |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 88 | if hasattr(test, '_testMethodName'): |
| 89 | if test._testMethodName == "runTest": |
| 90 | return str(test) |
| 91 | return test._testMethodName |
| 92 | else: |
| 93 | test_name = str(test) |
| 94 | whitespace_index = test_name.index(" ") |
| 95 | if whitespace_index != -1: |
| 96 | test_name = test_name[:whitespace_index] |
| 97 | return test_name |
| 98 | |
| 99 | def getTestId(self, test): |
| 100 | return test.id |
| 101 | |
| 102 | def addSuccess(self, test): |
| 103 | TestResult.addSuccess(self, test) |
| 104 | |
| 105 | def addError(self, test, err): |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 106 | self.init_suite(test) |
| 107 | self.current_failed = True |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 108 | TestResult.addError(self, test, err) |
| 109 | |
| 110 | err = self._exc_info_to_string(err, test) |
| 111 | |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 112 | self.messages.testStarted(self.getTestName(test)) |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 113 | self.messages.testError(self.getTestName(test), |
| 114 | message='Error', details=err) |
| 115 | |
| 116 | def find_error_value(self, err): |
| 117 | error_value = traceback.extract_tb(err) |
| 118 | error_value = error_value[-1][-1] |
| 119 | return error_value.split('assert')[-1].strip() |
| 120 | |
| 121 | def addFailure(self, test, err): |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 122 | self.init_suite(test) |
| 123 | self.current_failed = True |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 124 | TestResult.addFailure(self, test, err) |
| 125 | |
| 126 | error_value = smart_str(err[1]) |
| 127 | if not len(error_value): |
| 128 | # means it's test function and we have to extract value from traceback |
| 129 | error_value = self.find_error_value(err[2]) |
| 130 | |
| 131 | self_find_first = self.find_first(error_value) |
| 132 | self_find_second = self.find_second(error_value) |
| 133 | quotes = ["'", '"'] |
| 134 | if (self_find_first[0] == self_find_first[-1] and self_find_first[0] in quotes and |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 135 | self_find_second[0] == self_find_second[-1] and self_find_second[0] in quotes): |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 136 | # let's unescape strings to show sexy multiline diff in PyCharm. |
| 137 | # By default all caret return chars are escaped by testing framework |
| 138 | first = self._unescape(self_find_first) |
| 139 | second = self._unescape(self_find_second) |
| 140 | else: |
| 141 | first = second = "" |
| 142 | err = self._exc_info_to_string(err, test) |
| 143 | |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 144 | self.messages.testStarted(self.getTestName(test)) |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 145 | self.messages.testFailed(self.getTestName(test), |
| 146 | message='Failure', details=err, expected=first, actual=second) |
| 147 | |
| 148 | def addSkip(self, test, reason): |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 149 | self.init_suite(test) |
| 150 | self.current_failed = True |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 151 | self.messages.testIgnored(self.getTestName(test), message=reason) |
| 152 | |
| 153 | def __getSuite(self, test): |
| 154 | if hasattr(test, "suite"): |
| 155 | suite = strclass(test.suite) |
| 156 | suite_location = test.suite.location |
| 157 | location = test.suite.abs_location |
| 158 | if hasattr(test, "lineno"): |
| 159 | location = location + ":" + str(test.lineno) |
| 160 | else: |
| 161 | location = location + ":" + str(test.test.lineno) |
| 162 | else: |
| 163 | import inspect |
| 164 | |
| 165 | try: |
| 166 | source_file = inspect.getsourcefile(test.__class__) |
| 167 | if source_file: |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 168 | source_dir_splitted = source_file.split("/")[:-1] |
| 169 | source_dir = "/".join(source_dir_splitted) + "/" |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 170 | else: |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 171 | source_dir = "" |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 172 | except TypeError: |
| 173 | source_dir = "" |
| 174 | |
| 175 | suite = strclass(test.__class__) |
| 176 | suite_location = "python_uttestid://" + source_dir + suite |
| 177 | location = "python_uttestid://" + source_dir + str(test.id()) |
| 178 | |
| 179 | return (suite, location, suite_location) |
| 180 | |
| 181 | def startTest(self, test): |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 182 | self.current_failed = False |
| 183 | setattr(test, "startTime", datetime.datetime.now()) |
| 184 | |
| 185 | def init_suite(self, test): |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 186 | suite, location, suite_location = self.__getSuite(test) |
| 187 | if suite != self.current_suite: |
| 188 | if self.current_suite: |
| 189 | self.messages.testSuiteFinished(self.current_suite) |
| 190 | self.current_suite = suite |
| 191 | self.messages.testSuiteStarted(self.current_suite, location=suite_location) |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 192 | return location |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 193 | |
| 194 | def stopTest(self, test): |
| 195 | start = getattr(test, "startTime", datetime.datetime.now()) |
| 196 | d = datetime.datetime.now() - start |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 197 | duration = d.microseconds / 1000 + d.seconds * 1000 + d.days * 86400000 |
| 198 | if not self.subtest_suite: |
| 199 | if not self.current_failed: |
| 200 | location = self.init_suite(test) |
| 201 | self.messages.testStarted(self.getTestName(test), location=location) |
| 202 | self.messages.testFinished(self.getTestName(test), duration=int(duration)) |
| 203 | else: |
| 204 | self.messages.testSuiteFinished(self.subtest_suite) |
| 205 | self.subtest_suite = None |
| 206 | |
| 207 | |
| 208 | def addSubTest(self, test, subtest, err): |
| 209 | suite_name = self.getTestName(test) # + " (subTests)" |
| 210 | if not self.subtest_suite: |
| 211 | self.subtest_suite = suite_name |
| 212 | self.messages.testSuiteStarted(self.subtest_suite) |
| 213 | else: |
| 214 | if suite_name != self.subtest_suite: |
| 215 | self.messages.testSuiteFinished(self.subtest_suite) |
| 216 | self.subtest_suite = suite_name |
| 217 | self.messages.testSuiteStarted(self.subtest_suite) |
| 218 | |
| 219 | name = self.getTestName(subtest, True) |
| 220 | if err is not None: |
| 221 | error = self._exc_info_to_string(err, test) |
| 222 | self.messages.testStarted(name) |
| 223 | self.messages.testFailed(name, message='Failure', details=error) |
| 224 | else: |
| 225 | self.messages.testStarted(name) |
| 226 | self.messages.testFinished(name) |
| 227 | |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 228 | |
| 229 | def endLastSuite(self): |
| 230 | if self.current_suite: |
| 231 | self.messages.testSuiteFinished(self.current_suite) |
| 232 | self.current_suite = None |
| 233 | |
| 234 | def _unescape(self, text): |
| 235 | # do not use text.decode('string_escape'), it leads to problems with different string encodings given |
| 236 | return text.replace("\\n", "\n") |
| 237 | |
| Tor Norbye | 02cf98d | 2014-08-19 12:53:10 -0700 | [diff] [blame^] | 238 | |
| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame] | 239 | class TeamcityTestRunner(object): |
| 240 | def __init__(self, stream=sys.stdout): |
| 241 | self.stream = stream |
| 242 | |
| 243 | def _makeResult(self, **kwargs): |
| 244 | return TeamcityTestResult(self.stream, **kwargs) |
| 245 | |
| 246 | def run(self, test, **kwargs): |
| 247 | result = self._makeResult(**kwargs) |
| 248 | result.messages.testCount(test.countTestCases()) |
| 249 | test(result) |
| 250 | result.endLastSuite() |
| 251 | return result |