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