blob: ad989691bc99b4ef57c13a558e6590601ce86d9d [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
Nobuaki Sukegawaa6ab1f52015-11-28 15:04:39 +090020from __future__ import print_function
Roger Meier41ad4342015-03-24 22:30:40 +010021import datetime
22import json
23import multiprocessing
24import os
25import platform
26import re
27import subprocess
28import sys
29import time
30import traceback
31
Nobuaki Sukegawaa6ab1f52015-11-28 15:04:39 +090032from .compat import logfile_open, path_join, str_join
Nobuaki Sukegawa2de27002015-11-22 01:13:48 +090033from .test import TestEntry
Roger Meier41ad4342015-03-24 22:30:40 +010034
35LOG_DIR = 'log'
36RESULT_HTML = 'result.html'
37RESULT_JSON = 'results.json'
38FAIL_JSON = 'known_failures_%s.json'
39
40
41def generate_known_failures(testdir, overwrite, save, out):
42 def collect_failures(results):
43 success_index = 5
44 for r in results:
45 if not r[success_index]:
46 yield TestEntry.get_name(*r)
47 try:
Nobuaki Sukegawa378b7272016-01-03 17:04:50 +090048 with logfile_open(path_join(testdir, RESULT_JSON), 'r') as fp:
Roger Meier41ad4342015-03-24 22:30:40 +010049 results = json.load(fp)
50 except IOError:
51 sys.stderr.write('Unable to load last result. Did you run tests ?\n')
52 return False
53 fails = collect_failures(results['results'])
54 if not overwrite:
55 known = load_known_failures(testdir)
56 known.extend(fails)
57 fails = known
Nobuaki Sukegawaf5b795d2015-03-29 14:48:48 +090058 fails_json = json.dumps(sorted(set(fails)), indent=2, separators=(',', ': '))
Roger Meier41ad4342015-03-24 22:30:40 +010059 if save:
Nobuaki Sukegawae68ccc22015-12-13 21:45:39 +090060 with logfile_open(os.path.join(testdir, FAIL_JSON % platform.system()), 'w+') as fp:
Roger Meier41ad4342015-03-24 22:30:40 +010061 fp.write(fails_json)
62 sys.stdout.write('Successfully updated known failures.\n')
63 if out:
64 sys.stdout.write(fails_json)
65 sys.stdout.write('\n')
66 return True
67
68
69def load_known_failures(testdir):
70 try:
Nobuaki Sukegawa378b7272016-01-03 17:04:50 +090071 with logfile_open(path_join(testdir, FAIL_JSON % platform.system()), 'r') as fp:
Roger Meier41ad4342015-03-24 22:30:40 +010072 return json.load(fp)
73 except IOError:
74 return []
75
76
77class TestReporter(object):
78 # Unfortunately, standard library doesn't handle timezone well
79 # DATETIME_FORMAT = '%a %b %d %H:%M:%S %Z %Y'
80 DATETIME_FORMAT = '%a %b %d %H:%M:%S %Y'
81
82 def __init__(self):
83 self._log = multiprocessing.get_logger()
84 self._lock = multiprocessing.Lock()
85
86 @classmethod
Nobuaki Sukegawa783660a2015-04-12 00:32:40 +090087 def test_logfile(cls, test_name, prog_kind, dir=None):
Nobuaki Sukegawa378b7272016-01-03 17:04:50 +090088 relpath = path_join('log', '%s_%s.log' % (test_name, prog_kind))
Nobuaki Sukegawa2de27002015-11-22 01:13:48 +090089 return relpath if not dir else os.path.realpath(path_join(dir, relpath))
Roger Meier41ad4342015-03-24 22:30:40 +010090
91 def _start(self):
92 self._start_time = time.time()
93
94 @property
95 def _elapsed(self):
96 return time.time() - self._start_time
97
98 @classmethod
99 def _format_date(cls):
100 return '%s' % datetime.datetime.now().strftime(cls.DATETIME_FORMAT)
101
102 def _print_date(self):
103 self.out.write('%s\n' % self._format_date())
104
105 def _print_bar(self, out=None):
106 (out or self.out).write(
107 '======================================================================\n')
108
109 def _print_exec_time(self):
110 self.out.write('Test execution took {:.1f} seconds.\n'.format(self._elapsed))
111
112
113class ExecReporter(TestReporter):
114 def __init__(self, testdir, test, prog):
115 super(ExecReporter, self).__init__()
116 self._test = test
117 self._prog = prog
Nobuaki Sukegawa783660a2015-04-12 00:32:40 +0900118 self.logpath = self.test_logfile(test.name, prog.kind, testdir)
Roger Meier41ad4342015-03-24 22:30:40 +0100119 self.out = None
120
121 def begin(self):
122 self._start()
123 self._open()
124 if self.out and not self.out.closed:
125 self._print_header()
126 else:
127 self._log.debug('Output stream is not available.')
128
129 def end(self, returncode):
130 self._lock.acquire()
131 try:
132 if self.out and not self.out.closed:
133 self._print_footer(returncode)
134 self._close()
135 self.out = None
136 else:
137 self._log.debug('Output stream is not available.')
138 finally:
139 self._lock.release()
140
141 def killed(self):
Nobuaki Sukegawa2ba79442016-01-12 19:37:55 +0900142 self.out.write('Process is killed.\n')
143 self.end(None)
144
145 def died(self):
146 self.out.write('Process is died unexpectedly.\n')
Nobuaki Sukegawaa6ab1f52015-11-28 15:04:39 +0900147 self.end(None)
Roger Meier41ad4342015-03-24 22:30:40 +0100148
149 _init_failure_exprs = {
150 'server': list(map(re.compile, [
151 '[Aa]ddress already in use',
152 'Could not bind',
153 'EADDRINUSE',
154 ])),
155 'client': list(map(re.compile, [
156 '[Cc]onnection refused',
157 'Could not connect to localhost',
158 'ECONNREFUSED',
159 'No such file or directory', # domain socket
160 ])),
161 }
162
163 def maybe_false_positive(self):
164 """Searches through log file for socket bind error.
165 Returns True if suspicious expression is found, otherwise False"""
Roger Meier41ad4342015-03-24 22:30:40 +0100166 try:
167 if self.out and not self.out.closed:
168 self.out.flush()
Nobuaki Sukegawa9b35a7c2015-11-17 11:01:41 +0900169 exprs = self._init_failure_exprs[self._prog.kind]
Roger Meier41ad4342015-03-24 22:30:40 +0100170
Nobuaki Sukegawa9b35a7c2015-11-17 11:01:41 +0900171 def match(line):
172 for expr in exprs:
173 if expr.search(line):
174 return True
175
176 with logfile_open(self.logpath, 'r') as fp:
Roger Meier41ad4342015-03-24 22:30:40 +0100177 if any(map(match, fp)):
178 return True
179 except (KeyboardInterrupt, SystemExit):
180 raise
181 except Exception as ex:
182 self._log.warn('[%s]: Error while detecting false positive: %s' % (self._test.name, str(ex)))
183 self._log.info(traceback.print_exc())
184 return False
185
186 def _open(self):
Nobuaki Sukegawae68ccc22015-12-13 21:45:39 +0900187 self.out = logfile_open(self.logpath, 'w+')
Roger Meier41ad4342015-03-24 22:30:40 +0100188
189 def _close(self):
190 self.out.close()
191
192 def _print_header(self):
193 self._print_date()
Nobuaki Sukegawa2de27002015-11-22 01:13:48 +0900194 self.out.write('Executing: %s\n' % str_join(' ', self._prog.command))
Roger Meier41ad4342015-03-24 22:30:40 +0100195 self.out.write('Directory: %s\n' % self._prog.workdir)
196 self.out.write('config:delay: %s\n' % self._test.delay)
197 self.out.write('config:timeout: %s\n' % self._test.timeout)
198 self._print_bar()
199 self.out.flush()
200
201 def _print_footer(self, returncode=None):
202 self._print_bar()
203 if returncode is not None:
204 self.out.write('Return code: %d\n' % returncode)
205 else:
206 self.out.write('Process is killed.\n')
207 self._print_exec_time()
208 self._print_date()
209
210
211class SummaryReporter(TestReporter):
212 def __init__(self, testdir, concurrent=True):
213 super(SummaryReporter, self).__init__()
214 self.testdir = testdir
Nobuaki Sukegawa378b7272016-01-03 17:04:50 +0900215 self.logdir = path_join(testdir, LOG_DIR)
216 self.out_path = path_join(testdir, RESULT_JSON)
Roger Meier41ad4342015-03-24 22:30:40 +0100217 self.concurrent = concurrent
218 self.out = sys.stdout
219 self._platform = platform.system()
220 self._revision = self._get_revision()
221 self._tests = []
222 if not os.path.exists(self.logdir):
223 os.mkdir(self.logdir)
224 self._known_failures = load_known_failures(testdir)
225 self._unexpected_success = []
226 self._unexpected_failure = []
227 self._expected_failure = []
228 self._print_header()
229
230 def _get_revision(self):
231 p = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'],
232 cwd=self.testdir, stdout=subprocess.PIPE)
233 out, _ = p.communicate()
234 return out.strip()
235
236 def _format_test(self, test, with_result=True):
237 name = '%s-%s' % (test.server.name, test.client.name)
238 trans = '%s-%s' % (test.transport, test.socket)
239 if not with_result:
240 return '{:19s}{:13s}{:25s}'.format(name[:18], test.protocol[:12], trans[:24])
241 else:
242 result = 'success' if test.success else (
243 'timeout' if test.expired else 'failure')
244 result_string = '%s(%d)' % (result, test.returncode)
245 return '{:19s}{:13s}{:25s}{:s}\n'.format(name[:18], test.protocol[:12], trans[:24], result_string)
246
247 def _print_test_header(self):
248 self._print_bar()
249 self.out.write(
250 '{:19s}{:13s}{:25s}{:s}\n'.format('server-client:', 'protocol:', 'transport:', 'result:'))
251
252 def _print_header(self):
253 self._start()
254 self.out.writelines([
255 'Apache Thrift - Integration Test Suite\n',
256 ])
257 self._print_date()
258 self._print_test_header()
259
260 def _print_unexpected_failure(self):
261 if len(self._unexpected_failure) > 0:
262 self.out.writelines([
263 '*** Following %d failures were unexpected ***:\n' % len(self._unexpected_failure),
264 'If it is introduced by you, please fix it before submitting the code.\n',
265 # 'If not, please report at https://issues.apache.org/jira/browse/THRIFT\n',
266 ])
267 self._print_test_header()
268 for i in self._unexpected_failure:
269 self.out.write(self._format_test(self._tests[i]))
270 self._print_bar()
271 else:
272 self.out.write('No unexpected failures.\n')
273
274 def _print_unexpected_success(self):
275 if len(self._unexpected_success) > 0:
276 self.out.write(
277 'Following %d tests were known to fail but succeeded (it\'s normal):\n' % len(self._unexpected_success))
278 self._print_test_header()
279 for i in self._unexpected_success:
280 self.out.write(self._format_test(self._tests[i]))
281 self._print_bar()
282
Nobuaki Sukegawaf5b795d2015-03-29 14:48:48 +0900283 def _http_server_command(self, port):
284 if sys.version_info[0] < 3:
285 return 'python -m SimpleHTTPServer %d' % port
286 else:
287 return 'python -m http.server %d' % port
288
Roger Meier41ad4342015-03-24 22:30:40 +0100289 def _print_footer(self):
290 fail_count = len(self._expected_failure) + len(self._unexpected_failure)
291 self._print_bar()
292 self._print_unexpected_success()
293 self._print_unexpected_failure()
294 self._write_html_data()
295 self._assemble_log('unexpected failures', self._unexpected_failure)
296 self._assemble_log('known failures', self._expected_failure)
297 self.out.writelines([
298 'You can browse results at:\n',
299 '\tfile://%s/%s\n' % (self.testdir, RESULT_HTML),
Nobuaki Sukegawaf5b795d2015-03-29 14:48:48 +0900300 '# If you use Chrome, run:\n',
301 '# \tcd %s\n#\t%s\n' % (self.testdir, self._http_server_command(8001)),
302 '# then browse:\n',
Roger Meierbaa11012016-01-17 21:06:44 +0100303 '# \thttp://localhost:%d/test/%s\n' % (8001, RESULT_HTML),
Roger Meier41ad4342015-03-24 22:30:40 +0100304 'Full log for each test is here:\n',
305 '\ttest/log/client_server_protocol_transport_client.log\n',
306 '\ttest/log/client_server_protocol_transport_server.log\n',
307 '%d failed of %d tests in total.\n' % (fail_count, len(self._tests)),
308 ])
309 self._print_exec_time()
310 self._print_date()
311
312 def _render_result(self, test):
313 return [
314 test.server.name,
315 test.client.name,
316 test.protocol,
317 test.transport,
318 test.socket,
319 test.success,
320 test.as_expected,
321 test.returncode,
322 {
Nobuaki Sukegawa783660a2015-04-12 00:32:40 +0900323 'server': self.test_logfile(test.name, test.server.kind),
324 'client': self.test_logfile(test.name, test.client.kind),
Roger Meier41ad4342015-03-24 22:30:40 +0100325 },
326 ]
327
328 def _write_html_data(self):
329 """Writes JSON data to be read by result html"""
330 results = [self._render_result(r) for r in self._tests]
Nobuaki Sukegawae68ccc22015-12-13 21:45:39 +0900331 with logfile_open(self.out_path, 'w+') as fp:
Roger Meier41ad4342015-03-24 22:30:40 +0100332 fp.write(json.dumps({
333 'date': self._format_date(),
334 'revision': str(self._revision),
335 'platform': self._platform,
336 'duration': '{:.1f}'.format(self._elapsed),
337 'results': results,
338 }, indent=2))
339
340 def _assemble_log(self, title, indexes):
341 if len(indexes) > 0:
342 def add_prog_log(fp, test, prog_kind):
Nobuaki Sukegawaa6ab1f52015-11-28 15:04:39 +0900343 print('*************************** %s message ***************************' % prog_kind,
344 file=fp)
Nobuaki Sukegawa783660a2015-04-12 00:32:40 +0900345 path = self.test_logfile(test.name, prog_kind, self.testdir)
Nobuaki Sukegawaa6ab1f52015-11-28 15:04:39 +0900346 if os.path.exists(path):
347 with logfile_open(path, 'r') as prog_fp:
348 print(prog_fp.read(), file=fp)
Roger Meier41ad4342015-03-24 22:30:40 +0100349 filename = title.replace(' ', '_') + '.log'
Nobuaki Sukegawae68ccc22015-12-13 21:45:39 +0900350 with logfile_open(os.path.join(self.logdir, filename), 'w+') as fp:
Roger Meier41ad4342015-03-24 22:30:40 +0100351 for test in map(self._tests.__getitem__, indexes):
352 fp.write('TEST: [%s]\n' % test.name)
353 add_prog_log(fp, test, test.server.kind)
354 add_prog_log(fp, test, test.client.kind)
355 fp.write('**********************************************************************\n\n')
Nobuaki Sukegawaa6ab1f52015-11-28 15:04:39 +0900356 print('%s are logged to test/%s/%s' % (title.capitalize(), LOG_DIR, filename))
Roger Meier41ad4342015-03-24 22:30:40 +0100357
358 def end(self):
359 self._print_footer()
360 return len(self._unexpected_failure) == 0
361
362 def add_test(self, test_dict):
363 test = TestEntry(self.testdir, **test_dict)
364 self._lock.acquire()
365 try:
366 if not self.concurrent:
367 self.out.write(self._format_test(test, False))
368 self.out.flush()
369 self._tests.append(test)
370 return len(self._tests) - 1
371 finally:
372 self._lock.release()
373
374 def add_result(self, index, returncode, expired):
375 self._lock.acquire()
376 try:
377 failed = returncode is None or returncode != 0
378 test = self._tests[index]
379 known = test.name in self._known_failures
380 if failed:
381 if known:
382 self._log.debug('%s failed as expected' % test.name)
383 self._expected_failure.append(index)
384 else:
385 self._log.info('unexpected failure: %s' % test.name)
386 self._unexpected_failure.append(index)
387 elif known:
388 self._log.info('unexpected success: %s' % test.name)
389 self._unexpected_success.append(index)
390 test.success = not failed
391 test.returncode = returncode
392 test.expired = expired
393 test.as_expected = known == failed
394 if not self.concurrent:
395 result = 'success' if not failed else 'failure'
396 result_string = '%s(%d)' % (result, returncode)
397 self.out.write(result_string + '\n')
398 else:
399 self.out.write(self._format_test(test))
400 finally:
401 self._lock.release()