blob: 6ffc8e2d83669e585200a520f64bb84abbf2cb1d [file] [log] [blame]
Roger Meier41ad4342015-03-24 22:30:40 +01001#
2# Licensed to the Apache Software Foundation (ASF) under one
3# or more contributor license agreements. See the NOTICE file
4# distributed with this work for additional information
5# regarding copyright ownership. The ASF licenses this file
6# to you under the Apache License, Version 2.0 (the
7# "License"); you may not use this file except in compliance
8# with the License. You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing,
13# software distributed under the License is distributed on an
14# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15# KIND, either express or implied. See the License for the
16# specific language governing permissions and limitations
17# under the License.
18#
19
20import datetime
21import json
22import multiprocessing
23import os
24import platform
25import re
26import subprocess
27import sys
28import time
29import traceback
30
31from crossrunner.test import TestEntry
32
33LOG_DIR = 'log'
34RESULT_HTML = 'result.html'
35RESULT_JSON = 'results.json'
36FAIL_JSON = 'known_failures_%s.json'
37
38
39def generate_known_failures(testdir, overwrite, save, out):
40 def collect_failures(results):
41 success_index = 5
42 for r in results:
43 if not r[success_index]:
44 yield TestEntry.get_name(*r)
45 try:
46 with open(os.path.join(testdir, RESULT_JSON), 'r') as fp:
47 results = json.load(fp)
48 except IOError:
49 sys.stderr.write('Unable to load last result. Did you run tests ?\n')
50 return False
51 fails = collect_failures(results['results'])
52 if not overwrite:
53 known = load_known_failures(testdir)
54 known.extend(fails)
55 fails = known
Nobuaki Sukegawaf5b795d2015-03-29 14:48:48 +090056 fails_json = json.dumps(sorted(set(fails)), indent=2, separators=(',', ': '))
Roger Meier41ad4342015-03-24 22:30:40 +010057 if save:
58 with open(os.path.join(testdir, FAIL_JSON % platform.system()), 'w+') as fp:
59 fp.write(fails_json)
60 sys.stdout.write('Successfully updated known failures.\n')
61 if out:
62 sys.stdout.write(fails_json)
63 sys.stdout.write('\n')
64 return True
65
66
67def load_known_failures(testdir):
68 try:
69 with open(os.path.join(testdir, FAIL_JSON % platform.system()), 'r') as fp:
70 return json.load(fp)
71 except IOError:
72 return []
73
74
75class TestReporter(object):
76 # Unfortunately, standard library doesn't handle timezone well
77 # DATETIME_FORMAT = '%a %b %d %H:%M:%S %Z %Y'
78 DATETIME_FORMAT = '%a %b %d %H:%M:%S %Y'
79
80 def __init__(self):
81 self._log = multiprocessing.get_logger()
82 self._lock = multiprocessing.Lock()
83
84 @classmethod
Nobuaki Sukegawa783660a2015-04-12 00:32:40 +090085 def test_logfile(cls, test_name, prog_kind, dir=None):
86 relpath = os.path.join('log', '%s_%s.log' % (test_name, prog_kind))
Jens Geyeraad06de2015-11-21 14:43:56 +010087 return relpath if not dir else os.path.realpath(os.path.join(dir.decode(sys.getfilesystemencoding()), relpath.decode(sys.getfilesystemencoding())).encode(sys.getfilesystemencoding()))
Roger Meier41ad4342015-03-24 22:30:40 +010088
89 def _start(self):
90 self._start_time = time.time()
91
92 @property
93 def _elapsed(self):
94 return time.time() - self._start_time
95
96 @classmethod
97 def _format_date(cls):
98 return '%s' % datetime.datetime.now().strftime(cls.DATETIME_FORMAT)
99
100 def _print_date(self):
101 self.out.write('%s\n' % self._format_date())
102
103 def _print_bar(self, out=None):
104 (out or self.out).write(
105 '======================================================================\n')
106
107 def _print_exec_time(self):
108 self.out.write('Test execution took {:.1f} seconds.\n'.format(self._elapsed))
109
110
111class ExecReporter(TestReporter):
112 def __init__(self, testdir, test, prog):
113 super(ExecReporter, self).__init__()
114 self._test = test
115 self._prog = prog
Nobuaki Sukegawa783660a2015-04-12 00:32:40 +0900116 self.logpath = self.test_logfile(test.name, prog.kind, testdir)
Roger Meier41ad4342015-03-24 22:30:40 +0100117 self.out = None
118
119 def begin(self):
120 self._start()
121 self._open()
122 if self.out and not self.out.closed:
123 self._print_header()
124 else:
125 self._log.debug('Output stream is not available.')
126
127 def end(self, returncode):
128 self._lock.acquire()
129 try:
130 if self.out and not self.out.closed:
131 self._print_footer(returncode)
132 self._close()
133 self.out = None
134 else:
135 self._log.debug('Output stream is not available.')
136 finally:
137 self._lock.release()
138
139 def killed(self):
140 self._lock.acquire()
141 try:
142 if self.out and not self.out.closed:
143 self._print_footer()
144 self._close()
145 self.out = None
146 else:
147 self._log.debug('Output stream is not available.')
148 finally:
149 self._lock.release()
150
151 _init_failure_exprs = {
152 'server': list(map(re.compile, [
153 '[Aa]ddress already in use',
154 'Could not bind',
155 'EADDRINUSE',
156 ])),
157 'client': list(map(re.compile, [
158 '[Cc]onnection refused',
159 'Could not connect to localhost',
160 'ECONNREFUSED',
161 'No such file or directory', # domain socket
162 ])),
163 }
164
165 def maybe_false_positive(self):
166 """Searches through log file for socket bind error.
167 Returns True if suspicious expression is found, otherwise False"""
168 def match(line):
169 for expr in exprs:
170 if expr.search(line):
171 return True
172 try:
173 if self.out and not self.out.closed:
174 self.out.flush()
175 exprs = list(map(re.compile, self._init_failure_exprs[self._prog.kind]))
176
177 server_logfile = self.logpath
178 # need to handle unicode errors on Python 3
179 kwargs = {} if sys.version_info[0] < 3 else {'errors': 'replace'}
180 with open(server_logfile, 'r', **kwargs) as fp:
181 if any(map(match, fp)):
182 return True
183 except (KeyboardInterrupt, SystemExit):
184 raise
185 except Exception as ex:
186 self._log.warn('[%s]: Error while detecting false positive: %s' % (self._test.name, str(ex)))
187 self._log.info(traceback.print_exc())
188 return False
189
190 def _open(self):
191 self.out = open(self.logpath, 'w+')
192
193 def _close(self):
194 self.out.close()
195
196 def _print_header(self):
Jens Geyeraad06de2015-11-21 14:43:56 +0100197 tmp = list()
198 joined = ''
199 for item in self._prog.command:
200 tmp.append( item.decode(sys.getfilesystemencoding()))
201 joined = ' '.join(tmp).encode(sys.getfilesystemencoding())
Roger Meier41ad4342015-03-24 22:30:40 +0100202 self._print_date()
Jens Geyeraad06de2015-11-21 14:43:56 +0100203 self.out.write('Executing: %s\n' % joined)
Roger Meier41ad4342015-03-24 22:30:40 +0100204 self.out.write('Directory: %s\n' % self._prog.workdir)
205 self.out.write('config:delay: %s\n' % self._test.delay)
206 self.out.write('config:timeout: %s\n' % self._test.timeout)
207 self._print_bar()
208 self.out.flush()
209
210 def _print_footer(self, returncode=None):
211 self._print_bar()
212 if returncode is not None:
213 self.out.write('Return code: %d\n' % returncode)
214 else:
215 self.out.write('Process is killed.\n')
216 self._print_exec_time()
217 self._print_date()
218
219
220class SummaryReporter(TestReporter):
221 def __init__(self, testdir, concurrent=True):
222 super(SummaryReporter, self).__init__()
223 self.testdir = testdir
224 self.logdir = os.path.join(testdir, LOG_DIR)
225 self.out_path = os.path.join(testdir, RESULT_JSON)
226 self.concurrent = concurrent
227 self.out = sys.stdout
228 self._platform = platform.system()
229 self._revision = self._get_revision()
230 self._tests = []
231 if not os.path.exists(self.logdir):
232 os.mkdir(self.logdir)
233 self._known_failures = load_known_failures(testdir)
234 self._unexpected_success = []
235 self._unexpected_failure = []
236 self._expected_failure = []
237 self._print_header()
238
239 def _get_revision(self):
240 p = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'],
241 cwd=self.testdir, stdout=subprocess.PIPE)
242 out, _ = p.communicate()
243 return out.strip()
244
245 def _format_test(self, test, with_result=True):
246 name = '%s-%s' % (test.server.name, test.client.name)
247 trans = '%s-%s' % (test.transport, test.socket)
248 if not with_result:
249 return '{:19s}{:13s}{:25s}'.format(name[:18], test.protocol[:12], trans[:24])
250 else:
251 result = 'success' if test.success else (
252 'timeout' if test.expired else 'failure')
253 result_string = '%s(%d)' % (result, test.returncode)
254 return '{:19s}{:13s}{:25s}{:s}\n'.format(name[:18], test.protocol[:12], trans[:24], result_string)
255
256 def _print_test_header(self):
257 self._print_bar()
258 self.out.write(
259 '{:19s}{:13s}{:25s}{:s}\n'.format('server-client:', 'protocol:', 'transport:', 'result:'))
260
261 def _print_header(self):
262 self._start()
263 self.out.writelines([
264 'Apache Thrift - Integration Test Suite\n',
265 ])
266 self._print_date()
267 self._print_test_header()
268
269 def _print_unexpected_failure(self):
270 if len(self._unexpected_failure) > 0:
271 self.out.writelines([
272 '*** Following %d failures were unexpected ***:\n' % len(self._unexpected_failure),
273 'If it is introduced by you, please fix it before submitting the code.\n',
274 # 'If not, please report at https://issues.apache.org/jira/browse/THRIFT\n',
275 ])
276 self._print_test_header()
277 for i in self._unexpected_failure:
278 self.out.write(self._format_test(self._tests[i]))
279 self._print_bar()
280 else:
281 self.out.write('No unexpected failures.\n')
282
283 def _print_unexpected_success(self):
284 if len(self._unexpected_success) > 0:
285 self.out.write(
286 'Following %d tests were known to fail but succeeded (it\'s normal):\n' % len(self._unexpected_success))
287 self._print_test_header()
288 for i in self._unexpected_success:
289 self.out.write(self._format_test(self._tests[i]))
290 self._print_bar()
291
Nobuaki Sukegawaf5b795d2015-03-29 14:48:48 +0900292 def _http_server_command(self, port):
293 if sys.version_info[0] < 3:
294 return 'python -m SimpleHTTPServer %d' % port
295 else:
296 return 'python -m http.server %d' % port
297
Roger Meier41ad4342015-03-24 22:30:40 +0100298 def _print_footer(self):
299 fail_count = len(self._expected_failure) + len(self._unexpected_failure)
300 self._print_bar()
301 self._print_unexpected_success()
302 self._print_unexpected_failure()
303 self._write_html_data()
304 self._assemble_log('unexpected failures', self._unexpected_failure)
305 self._assemble_log('known failures', self._expected_failure)
306 self.out.writelines([
307 'You can browse results at:\n',
308 '\tfile://%s/%s\n' % (self.testdir, RESULT_HTML),
Nobuaki Sukegawaf5b795d2015-03-29 14:48:48 +0900309 '# If you use Chrome, run:\n',
310 '# \tcd %s\n#\t%s\n' % (self.testdir, self._http_server_command(8001)),
311 '# then browse:\n',
312 '# \thttp://localhost:%d/%s\n' % (8001, RESULT_HTML),
Roger Meier41ad4342015-03-24 22:30:40 +0100313 'Full log for each test is here:\n',
314 '\ttest/log/client_server_protocol_transport_client.log\n',
315 '\ttest/log/client_server_protocol_transport_server.log\n',
316 '%d failed of %d tests in total.\n' % (fail_count, len(self._tests)),
317 ])
318 self._print_exec_time()
319 self._print_date()
320
321 def _render_result(self, test):
322 return [
323 test.server.name,
324 test.client.name,
325 test.protocol,
326 test.transport,
327 test.socket,
328 test.success,
329 test.as_expected,
330 test.returncode,
331 {
Nobuaki Sukegawa783660a2015-04-12 00:32:40 +0900332 'server': self.test_logfile(test.name, test.server.kind),
333 'client': self.test_logfile(test.name, test.client.kind),
Roger Meier41ad4342015-03-24 22:30:40 +0100334 },
335 ]
336
337 def _write_html_data(self):
338 """Writes JSON data to be read by result html"""
339 results = [self._render_result(r) for r in self._tests]
340 with open(self.out_path, 'w+') as fp:
341 fp.write(json.dumps({
342 'date': self._format_date(),
343 'revision': str(self._revision),
344 'platform': self._platform,
345 'duration': '{:.1f}'.format(self._elapsed),
346 'results': results,
347 }, indent=2))
348
349 def _assemble_log(self, title, indexes):
350 if len(indexes) > 0:
351 def add_prog_log(fp, test, prog_kind):
352 fp.write('*************************** %s message ***************************\n'
353 % prog_kind)
Nobuaki Sukegawa783660a2015-04-12 00:32:40 +0900354 path = self.test_logfile(test.name, prog_kind, self.testdir)
Roger Meier41ad4342015-03-24 22:30:40 +0100355 kwargs = {} if sys.version_info[0] < 3 else {'errors': 'replace'}
356 with open(path, 'r', **kwargs) as prog_fp:
357 fp.write(prog_fp.read())
358 filename = title.replace(' ', '_') + '.log'
359 with open(os.path.join(self.logdir, filename), 'w+') as fp:
360 for test in map(self._tests.__getitem__, indexes):
361 fp.write('TEST: [%s]\n' % test.name)
362 add_prog_log(fp, test, test.server.kind)
363 add_prog_log(fp, test, test.client.kind)
364 fp.write('**********************************************************************\n\n')
365 self.out.write('%s are logged to test/%s/%s\n' % (title.capitalize(), LOG_DIR, filename))
366
367 def end(self):
368 self._print_footer()
369 return len(self._unexpected_failure) == 0
370
371 def add_test(self, test_dict):
372 test = TestEntry(self.testdir, **test_dict)
373 self._lock.acquire()
374 try:
375 if not self.concurrent:
376 self.out.write(self._format_test(test, False))
377 self.out.flush()
378 self._tests.append(test)
379 return len(self._tests) - 1
380 finally:
381 self._lock.release()
382
383 def add_result(self, index, returncode, expired):
384 self._lock.acquire()
385 try:
386 failed = returncode is None or returncode != 0
387 test = self._tests[index]
388 known = test.name in self._known_failures
389 if failed:
390 if known:
391 self._log.debug('%s failed as expected' % test.name)
392 self._expected_failure.append(index)
393 else:
394 self._log.info('unexpected failure: %s' % test.name)
395 self._unexpected_failure.append(index)
396 elif known:
397 self._log.info('unexpected success: %s' % test.name)
398 self._unexpected_success.append(index)
399 test.success = not failed
400 test.returncode = returncode
401 test.expired = expired
402 test.as_expected = known == failed
403 if not self.concurrent:
404 result = 'success' if not failed else 'failure'
405 result_string = '%s(%d)' % (result, returncode)
406 self.out.write(result_string + '\n')
407 else:
408 self.out.write(self._format_test(test))
409 finally:
410 self._lock.release()