blob: 99b30595a1919e02a1a16a7ca8c75a6c63c50861 [file] [log] [blame]
Tor Norbye3a2425a2013-11-04 10:16:08 -08001import traceback, sys
2from unittest import TestResult
3import datetime
4
5from tcmessages import TeamcityServiceMessages
6
7PYTHON_VERSION_MAJOR = sys.version_info[0]
8
Tor Norbye02cf98d2014-08-19 12:53:10 -07009
Tor Norbye3a2425a2013-11-04 10:16:08 -080010def strclass(cls):
11 if not cls.__name__:
12 return cls.__module__
13 return "%s.%s" % (cls.__module__, cls.__name__)
14
Tor Norbye02cf98d2014-08-19 12:53:10 -070015
Tor Norbye3a2425a2013-11-04 10:16:08 -080016def smart_str(s):
Tor Norbye02cf98d2014-08-19 12:53:10 -070017 encoding = 'utf-8'
18 errors = 'strict'
Tor Norbye3a2425a2013-11-04 10:16:08 -080019 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 Norbye02cf98d2014-08-19 12:53:10 -070038
Tor Norbye3a2425a2013-11-04 10:16:08 -080039class 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 Norbye02cf98d2014-08-19 12:53:10 -070047 self.current_failed = False
Tor Norbye3a2425a2013-11-04 10:16:08 -080048 self.current_suite = None
Tor Norbye02cf98d2014-08-19 12:53:10 -070049 self.subtest_suite = None
Tor Norbye3a2425a2013-11-04 10:16:08 -080050
51 def find_first(self, val):
52 quot = val[0]
53 count = 1
54 quote_ind = val[count:].find(quot)
Tor Norbye02cf98d2014-08-19 12:53:10 -070055 while quote_ind != -1 and val[count + quote_ind - 1] == "\\":
Tor Norbye3a2425a2013-11-04 10:16:08 -080056 count = count + quote_ind + 1
57 quote_ind = val[count:].find(quot)
58
Tor Norbye02cf98d2014-08-19 12:53:10 -070059 return val[0:quote_ind + count + 1]
Tor Norbye3a2425a2013-11-04 10:16:08 -080060
61 def find_second(self, val):
62 val_index = val.find("!=")
63 if val_index != -1:
64 count = 1
Tor Norbye02cf98d2014-08-19 12:53:10 -070065 val = val[val_index + 2:].strip()
Tor Norbye3a2425a2013-11-04 10:16:08 -080066 quot = val[0]
67 quote_ind = val[count:].find(quot)
Tor Norbye02cf98d2014-08-19 12:53:10 -070068 while quote_ind != -1 and val[count + quote_ind - 1] == "\\":
Tor Norbye3a2425a2013-11-04 10:16:08 -080069 count = count + quote_ind + 1
70 quote_ind = val[count:].find(quot)
Tor Norbye02cf98d2014-08-19 12:53:10 -070071 return val[0:quote_ind + count + 1]
Tor Norbye3a2425a2013-11-04 10:16:08 -080072
73 else:
74 quot = val[-1]
Tor Norbye02cf98d2014-08-19 12:53:10 -070075 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 Norbye3a2425a2013-11-04 10:16:08 -080078 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 Norbye02cf98d2014-08-19 12:53:10 -070084 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 Norbye3a2425a2013-11-04 10:16:08 -080088 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 Norbye02cf98d2014-08-19 12:53:10 -0700106 self.init_suite(test)
107 self.current_failed = True
Tor Norbye3a2425a2013-11-04 10:16:08 -0800108 TestResult.addError(self, test, err)
109
110 err = self._exc_info_to_string(err, test)
111
Tor Norbye02cf98d2014-08-19 12:53:10 -0700112 self.messages.testStarted(self.getTestName(test))
Tor Norbye3a2425a2013-11-04 10:16:08 -0800113 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 Norbye02cf98d2014-08-19 12:53:10 -0700122 self.init_suite(test)
123 self.current_failed = True
Tor Norbye3a2425a2013-11-04 10:16:08 -0800124 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 Norbye02cf98d2014-08-19 12:53:10 -0700135 self_find_second[0] == self_find_second[-1] and self_find_second[0] in quotes):
Tor Norbye3a2425a2013-11-04 10:16:08 -0800136 # 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 Norbye02cf98d2014-08-19 12:53:10 -0700144 self.messages.testStarted(self.getTestName(test))
Tor Norbye3a2425a2013-11-04 10:16:08 -0800145 self.messages.testFailed(self.getTestName(test),
146 message='Failure', details=err, expected=first, actual=second)
147
148 def addSkip(self, test, reason):
Tor Norbye02cf98d2014-08-19 12:53:10 -0700149 self.init_suite(test)
150 self.current_failed = True
Tor Norbye3a2425a2013-11-04 10:16:08 -0800151 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 Norbye02cf98d2014-08-19 12:53:10 -0700168 source_dir_splitted = source_file.split("/")[:-1]
169 source_dir = "/".join(source_dir_splitted) + "/"
Tor Norbye3a2425a2013-11-04 10:16:08 -0800170 else:
Tor Norbye02cf98d2014-08-19 12:53:10 -0700171 source_dir = ""
Tor Norbye3a2425a2013-11-04 10:16:08 -0800172 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 Norbye02cf98d2014-08-19 12:53:10 -0700182 self.current_failed = False
183 setattr(test, "startTime", datetime.datetime.now())
184
185 def init_suite(self, test):
Tor Norbye3a2425a2013-11-04 10:16:08 -0800186 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 Norbye02cf98d2014-08-19 12:53:10 -0700192 return location
Tor Norbye3a2425a2013-11-04 10:16:08 -0800193
194 def stopTest(self, test):
195 start = getattr(test, "startTime", datetime.datetime.now())
196 d = datetime.datetime.now() - start
Tor Norbye02cf98d2014-08-19 12:53:10 -0700197 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 Norbye3a2425a2013-11-04 10:16:08 -0800228
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 Norbye02cf98d2014-08-19 12:53:10 -0700238
Tor Norbye3a2425a2013-11-04 10:16:08 -0800239class 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