| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 1 | # | 
 | 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 Sukegawa | a6ab1f5 | 2015-11-28 15:04:39 +0900 | [diff] [blame] | 20 | from __future__ import print_function | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 21 | import datetime | 
 | 22 | import json | 
 | 23 | import multiprocessing | 
 | 24 | import os | 
 | 25 | import platform | 
 | 26 | import re | 
 | 27 | import subprocess | 
 | 28 | import sys | 
 | 29 | import time | 
 | 30 | import traceback | 
 | 31 |  | 
| Nobuaki Sukegawa | a6ab1f5 | 2015-11-28 15:04:39 +0900 | [diff] [blame] | 32 | from .compat import logfile_open, path_join, str_join | 
| Nobuaki Sukegawa | 2de2700 | 2015-11-22 01:13:48 +0900 | [diff] [blame] | 33 | from .test import TestEntry | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 34 |  | 
 | 35 | LOG_DIR = 'log' | 
 | 36 | RESULT_HTML = 'result.html' | 
 | 37 | RESULT_JSON = 'results.json' | 
 | 38 | FAIL_JSON = 'known_failures_%s.json' | 
 | 39 |  | 
 | 40 |  | 
 | 41 | def 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 Sukegawa | a6ab1f5 | 2015-11-28 15:04:39 +0900 | [diff] [blame] | 48 |     with logfile_open(os.path.join(testdir, RESULT_JSON), 'r') as fp: | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 49 |       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 Sukegawa | f5b795d | 2015-03-29 14:48:48 +0900 | [diff] [blame] | 58 |   fails_json = json.dumps(sorted(set(fails)), indent=2, separators=(',', ': ')) | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 59 |   if save: | 
| Nobuaki Sukegawa | e68ccc2 | 2015-12-13 21:45:39 +0900 | [diff] [blame] | 60 |     with logfile_open(os.path.join(testdir, FAIL_JSON % platform.system()), 'w+') as fp: | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 61 |       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 |  | 
 | 69 | def load_known_failures(testdir): | 
 | 70 |   try: | 
| Nobuaki Sukegawa | a6ab1f5 | 2015-11-28 15:04:39 +0900 | [diff] [blame] | 71 |     with logfile_open(os.path.join(testdir, FAIL_JSON % platform.system()), 'r') as fp: | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 72 |       return json.load(fp) | 
 | 73 |   except IOError: | 
 | 74 |     return [] | 
 | 75 |  | 
 | 76 |  | 
 | 77 | class 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 Sukegawa | 783660a | 2015-04-12 00:32:40 +0900 | [diff] [blame] | 87 |   def test_logfile(cls, test_name, prog_kind, dir=None): | 
 | 88 |     relpath = os.path.join('log', '%s_%s.log' % (test_name, prog_kind)) | 
| Nobuaki Sukegawa | 2de2700 | 2015-11-22 01:13:48 +0900 | [diff] [blame] | 89 |     return relpath if not dir else os.path.realpath(path_join(dir, relpath)) | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 90 |  | 
 | 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 |  | 
 | 113 | class ExecReporter(TestReporter): | 
 | 114 |   def __init__(self, testdir, test, prog): | 
 | 115 |     super(ExecReporter, self).__init__() | 
 | 116 |     self._test = test | 
 | 117 |     self._prog = prog | 
| Nobuaki Sukegawa | 783660a | 2015-04-12 00:32:40 +0900 | [diff] [blame] | 118 |     self.logpath = self.test_logfile(test.name, prog.kind, testdir) | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 119 |     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 Sukegawa | a6ab1f5 | 2015-11-28 15:04:39 +0900 | [diff] [blame] | 142 |     self.end(None) | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 143 |  | 
 | 144 |   _init_failure_exprs = { | 
 | 145 |     'server': list(map(re.compile, [ | 
 | 146 |       '[Aa]ddress already in use', | 
 | 147 |       'Could not bind', | 
 | 148 |       'EADDRINUSE', | 
 | 149 |     ])), | 
 | 150 |     'client': list(map(re.compile, [ | 
 | 151 |       '[Cc]onnection refused', | 
 | 152 |       'Could not connect to localhost', | 
 | 153 |       'ECONNREFUSED', | 
 | 154 |       'No such file or directory',  # domain socket | 
 | 155 |     ])), | 
 | 156 |   } | 
 | 157 |  | 
 | 158 |   def maybe_false_positive(self): | 
 | 159 |     """Searches through log file for socket bind error. | 
 | 160 |     Returns True if suspicious expression is found, otherwise False""" | 
 | 161 |     def match(line): | 
 | 162 |       for expr in exprs: | 
 | 163 |         if expr.search(line): | 
 | 164 |           return True | 
 | 165 |     try: | 
 | 166 |       if self.out and not self.out.closed: | 
 | 167 |         self.out.flush() | 
 | 168 |       exprs = list(map(re.compile, self._init_failure_exprs[self._prog.kind])) | 
 | 169 |  | 
 | 170 |       server_logfile = self.logpath | 
 | 171 |       # need to handle unicode errors on Python 3 | 
| Nobuaki Sukegawa | a6ab1f5 | 2015-11-28 15:04:39 +0900 | [diff] [blame] | 172 |       with logfile_open(server_logfile, 'r') as fp: | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 173 |         if any(map(match, fp)): | 
 | 174 |           return True | 
 | 175 |     except (KeyboardInterrupt, SystemExit): | 
 | 176 |       raise | 
 | 177 |     except Exception as ex: | 
 | 178 |       self._log.warn('[%s]: Error while detecting false positive: %s' % (self._test.name, str(ex))) | 
 | 179 |       self._log.info(traceback.print_exc()) | 
 | 180 |     return False | 
 | 181 |  | 
 | 182 |   def _open(self): | 
| Nobuaki Sukegawa | e68ccc2 | 2015-12-13 21:45:39 +0900 | [diff] [blame] | 183 |     self.out = logfile_open(self.logpath, 'w+') | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 184 |  | 
 | 185 |   def _close(self): | 
 | 186 |     self.out.close() | 
 | 187 |  | 
 | 188 |   def _print_header(self): | 
 | 189 |     self._print_date() | 
| Nobuaki Sukegawa | 2de2700 | 2015-11-22 01:13:48 +0900 | [diff] [blame] | 190 |     self.out.write('Executing: %s\n' % str_join(' ', self._prog.command)) | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 191 |     self.out.write('Directory: %s\n' % self._prog.workdir) | 
 | 192 |     self.out.write('config:delay: %s\n' % self._test.delay) | 
 | 193 |     self.out.write('config:timeout: %s\n' % self._test.timeout) | 
 | 194 |     self._print_bar() | 
 | 195 |     self.out.flush() | 
 | 196 |  | 
 | 197 |   def _print_footer(self, returncode=None): | 
 | 198 |     self._print_bar() | 
 | 199 |     if returncode is not None: | 
 | 200 |       self.out.write('Return code: %d\n' % returncode) | 
 | 201 |     else: | 
 | 202 |       self.out.write('Process is killed.\n') | 
 | 203 |     self._print_exec_time() | 
 | 204 |     self._print_date() | 
 | 205 |  | 
 | 206 |  | 
 | 207 | class SummaryReporter(TestReporter): | 
 | 208 |   def __init__(self, testdir, concurrent=True): | 
 | 209 |     super(SummaryReporter, self).__init__() | 
 | 210 |     self.testdir = testdir | 
 | 211 |     self.logdir = os.path.join(testdir, LOG_DIR) | 
 | 212 |     self.out_path = os.path.join(testdir, RESULT_JSON) | 
 | 213 |     self.concurrent = concurrent | 
 | 214 |     self.out = sys.stdout | 
 | 215 |     self._platform = platform.system() | 
 | 216 |     self._revision = self._get_revision() | 
 | 217 |     self._tests = [] | 
 | 218 |     if not os.path.exists(self.logdir): | 
 | 219 |       os.mkdir(self.logdir) | 
 | 220 |     self._known_failures = load_known_failures(testdir) | 
 | 221 |     self._unexpected_success = [] | 
 | 222 |     self._unexpected_failure = [] | 
 | 223 |     self._expected_failure = [] | 
 | 224 |     self._print_header() | 
 | 225 |  | 
 | 226 |   def _get_revision(self): | 
 | 227 |     p = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'], | 
 | 228 |                          cwd=self.testdir, stdout=subprocess.PIPE) | 
 | 229 |     out, _ = p.communicate() | 
 | 230 |     return out.strip() | 
 | 231 |  | 
 | 232 |   def _format_test(self, test, with_result=True): | 
 | 233 |     name = '%s-%s' % (test.server.name, test.client.name) | 
 | 234 |     trans = '%s-%s' % (test.transport, test.socket) | 
 | 235 |     if not with_result: | 
 | 236 |       return '{:19s}{:13s}{:25s}'.format(name[:18], test.protocol[:12], trans[:24]) | 
 | 237 |     else: | 
 | 238 |       result = 'success' if test.success else ( | 
 | 239 |           'timeout' if test.expired else 'failure') | 
 | 240 |       result_string = '%s(%d)' % (result, test.returncode) | 
 | 241 |       return '{:19s}{:13s}{:25s}{:s}\n'.format(name[:18], test.protocol[:12], trans[:24], result_string) | 
 | 242 |  | 
 | 243 |   def _print_test_header(self): | 
 | 244 |     self._print_bar() | 
 | 245 |     self.out.write( | 
 | 246 |       '{:19s}{:13s}{:25s}{:s}\n'.format('server-client:', 'protocol:', 'transport:', 'result:')) | 
 | 247 |  | 
 | 248 |   def _print_header(self): | 
 | 249 |     self._start() | 
 | 250 |     self.out.writelines([ | 
 | 251 |       'Apache Thrift - Integration Test Suite\n', | 
 | 252 |     ]) | 
 | 253 |     self._print_date() | 
 | 254 |     self._print_test_header() | 
 | 255 |  | 
 | 256 |   def _print_unexpected_failure(self): | 
 | 257 |     if len(self._unexpected_failure) > 0: | 
 | 258 |       self.out.writelines([ | 
 | 259 |         '*** Following %d failures were unexpected ***:\n' % len(self._unexpected_failure), | 
 | 260 |         'If it is introduced by you, please fix it before submitting the code.\n', | 
 | 261 |         # 'If not, please report at https://issues.apache.org/jira/browse/THRIFT\n', | 
 | 262 |       ]) | 
 | 263 |       self._print_test_header() | 
 | 264 |       for i in self._unexpected_failure: | 
 | 265 |         self.out.write(self._format_test(self._tests[i])) | 
 | 266 |       self._print_bar() | 
 | 267 |     else: | 
 | 268 |       self.out.write('No unexpected failures.\n') | 
 | 269 |  | 
 | 270 |   def _print_unexpected_success(self): | 
 | 271 |     if len(self._unexpected_success) > 0: | 
 | 272 |       self.out.write( | 
 | 273 |         'Following %d tests were known to fail but succeeded (it\'s normal):\n' % len(self._unexpected_success)) | 
 | 274 |       self._print_test_header() | 
 | 275 |       for i in self._unexpected_success: | 
 | 276 |         self.out.write(self._format_test(self._tests[i])) | 
 | 277 |       self._print_bar() | 
 | 278 |  | 
| Nobuaki Sukegawa | f5b795d | 2015-03-29 14:48:48 +0900 | [diff] [blame] | 279 |   def _http_server_command(self, port): | 
 | 280 |     if sys.version_info[0] < 3: | 
 | 281 |       return 'python -m SimpleHTTPServer %d' % port | 
 | 282 |     else: | 
 | 283 |       return 'python -m http.server %d' % port | 
 | 284 |  | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 285 |   def _print_footer(self): | 
 | 286 |     fail_count = len(self._expected_failure) + len(self._unexpected_failure) | 
 | 287 |     self._print_bar() | 
 | 288 |     self._print_unexpected_success() | 
 | 289 |     self._print_unexpected_failure() | 
 | 290 |     self._write_html_data() | 
 | 291 |     self._assemble_log('unexpected failures', self._unexpected_failure) | 
 | 292 |     self._assemble_log('known failures', self._expected_failure) | 
 | 293 |     self.out.writelines([ | 
 | 294 |       'You can browse results at:\n', | 
 | 295 |       '\tfile://%s/%s\n' % (self.testdir, RESULT_HTML), | 
| Nobuaki Sukegawa | f5b795d | 2015-03-29 14:48:48 +0900 | [diff] [blame] | 296 |       '# If you use Chrome, run:\n', | 
 | 297 |       '# \tcd %s\n#\t%s\n' % (self.testdir, self._http_server_command(8001)), | 
 | 298 |       '# then browse:\n', | 
 | 299 |       '# \thttp://localhost:%d/%s\n' % (8001, RESULT_HTML), | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 300 |       'Full log for each test is here:\n', | 
 | 301 |       '\ttest/log/client_server_protocol_transport_client.log\n', | 
 | 302 |       '\ttest/log/client_server_protocol_transport_server.log\n', | 
 | 303 |       '%d failed of %d tests in total.\n' % (fail_count, len(self._tests)), | 
 | 304 |     ]) | 
 | 305 |     self._print_exec_time() | 
 | 306 |     self._print_date() | 
 | 307 |  | 
 | 308 |   def _render_result(self, test): | 
 | 309 |     return [ | 
 | 310 |       test.server.name, | 
 | 311 |       test.client.name, | 
 | 312 |       test.protocol, | 
 | 313 |       test.transport, | 
 | 314 |       test.socket, | 
 | 315 |       test.success, | 
 | 316 |       test.as_expected, | 
 | 317 |       test.returncode, | 
 | 318 |       { | 
| Nobuaki Sukegawa | 783660a | 2015-04-12 00:32:40 +0900 | [diff] [blame] | 319 |         'server': self.test_logfile(test.name, test.server.kind), | 
 | 320 |         'client': self.test_logfile(test.name, test.client.kind), | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 321 |       }, | 
 | 322 |     ] | 
 | 323 |  | 
 | 324 |   def _write_html_data(self): | 
 | 325 |     """Writes JSON data to be read by result html""" | 
 | 326 |     results = [self._render_result(r) for r in self._tests] | 
| Nobuaki Sukegawa | e68ccc2 | 2015-12-13 21:45:39 +0900 | [diff] [blame] | 327 |     with logfile_open(self.out_path, 'w+') as fp: | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 328 |       fp.write(json.dumps({ | 
 | 329 |         'date': self._format_date(), | 
 | 330 |         'revision': str(self._revision), | 
 | 331 |         'platform': self._platform, | 
 | 332 |         'duration': '{:.1f}'.format(self._elapsed), | 
 | 333 |         'results': results, | 
 | 334 |       }, indent=2)) | 
 | 335 |  | 
 | 336 |   def _assemble_log(self, title, indexes): | 
 | 337 |     if len(indexes) > 0: | 
 | 338 |       def add_prog_log(fp, test, prog_kind): | 
| Nobuaki Sukegawa | a6ab1f5 | 2015-11-28 15:04:39 +0900 | [diff] [blame] | 339 |         print('*************************** %s message ***************************' % prog_kind, | 
 | 340 |               file=fp) | 
| Nobuaki Sukegawa | 783660a | 2015-04-12 00:32:40 +0900 | [diff] [blame] | 341 |         path = self.test_logfile(test.name, prog_kind, self.testdir) | 
| Nobuaki Sukegawa | a6ab1f5 | 2015-11-28 15:04:39 +0900 | [diff] [blame] | 342 |         if os.path.exists(path): | 
 | 343 |           with logfile_open(path, 'r') as prog_fp: | 
 | 344 |             print(prog_fp.read(), file=fp) | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 345 |       filename = title.replace(' ', '_') + '.log' | 
| Nobuaki Sukegawa | e68ccc2 | 2015-12-13 21:45:39 +0900 | [diff] [blame] | 346 |       with logfile_open(os.path.join(self.logdir, filename), 'w+') as fp: | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 347 |         for test in map(self._tests.__getitem__, indexes): | 
 | 348 |           fp.write('TEST: [%s]\n' % test.name) | 
 | 349 |           add_prog_log(fp, test, test.server.kind) | 
 | 350 |           add_prog_log(fp, test, test.client.kind) | 
 | 351 |           fp.write('**********************************************************************\n\n') | 
| Nobuaki Sukegawa | a6ab1f5 | 2015-11-28 15:04:39 +0900 | [diff] [blame] | 352 |       print('%s are logged to test/%s/%s' % (title.capitalize(), LOG_DIR, filename)) | 
| Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 353 |  | 
 | 354 |   def end(self): | 
 | 355 |     self._print_footer() | 
 | 356 |     return len(self._unexpected_failure) == 0 | 
 | 357 |  | 
 | 358 |   def add_test(self, test_dict): | 
 | 359 |     test = TestEntry(self.testdir, **test_dict) | 
 | 360 |     self._lock.acquire() | 
 | 361 |     try: | 
 | 362 |       if not self.concurrent: | 
 | 363 |         self.out.write(self._format_test(test, False)) | 
 | 364 |         self.out.flush() | 
 | 365 |       self._tests.append(test) | 
 | 366 |       return len(self._tests) - 1 | 
 | 367 |     finally: | 
 | 368 |       self._lock.release() | 
 | 369 |  | 
 | 370 |   def add_result(self, index, returncode, expired): | 
 | 371 |     self._lock.acquire() | 
 | 372 |     try: | 
 | 373 |       failed = returncode is None or returncode != 0 | 
 | 374 |       test = self._tests[index] | 
 | 375 |       known = test.name in self._known_failures | 
 | 376 |       if failed: | 
 | 377 |         if known: | 
 | 378 |           self._log.debug('%s failed as expected' % test.name) | 
 | 379 |           self._expected_failure.append(index) | 
 | 380 |         else: | 
 | 381 |           self._log.info('unexpected failure: %s' % test.name) | 
 | 382 |           self._unexpected_failure.append(index) | 
 | 383 |       elif known: | 
 | 384 |         self._log.info('unexpected success: %s' % test.name) | 
 | 385 |         self._unexpected_success.append(index) | 
 | 386 |       test.success = not failed | 
 | 387 |       test.returncode = returncode | 
 | 388 |       test.expired = expired | 
 | 389 |       test.as_expected = known == failed | 
 | 390 |       if not self.concurrent: | 
 | 391 |         result = 'success' if not failed else 'failure' | 
 | 392 |         result_string = '%s(%d)' % (result, returncode) | 
 | 393 |         self.out.write(result_string + '\n') | 
 | 394 |       else: | 
 | 395 |         self.out.write(self._format_test(test)) | 
 | 396 |     finally: | 
 | 397 |       self._lock.release() |