tfiala created this revision. tfiala added reviewers: zturner, clayborg, labath. tfiala added a subscriber: lldb-commits.
For all the parallel test runners, provide a pure-Python mechanism for timing out dotest inferior processes that run for too long. Stock OS X and Windows do not have a built-in timeout helper app. This allows those systems to support timeouts. It also transforms the timeout from being implemented via a system process to costing a single thread. Tested on Linux and OS X with positive timeout verification. Greg: this also comments out one of the curses cleanup calls into keyboard handling. I found that always hung the test shutdown on the curses runner. (Attached with a python debugger to verify). http://reviews.llvm.org/D13124 Files: test/curses_results.py test/dosep.py
Index: test/dosep.py =================================================================== --- test/dosep.py +++ test/dosep.py @@ -45,28 +45,10 @@ import subprocess import sys import threading -import test_results import dotest_channels import dotest_args -def get_timeout_command(): - """Search for a suitable timeout command.""" - if not sys.platform.startswith("win32"): - try: - subprocess.call("timeout", stderr=subprocess.PIPE) - return "timeout" - except OSError: - pass - try: - subprocess.call("gtimeout", stderr=subprocess.PIPE) - return "gtimeout" - except OSError: - pass - return None - -timeout_command = get_timeout_command() - # Status codes for running command with timeout. eTimedOut, ePassed, eFailed = 124, 0, 1 @@ -171,7 +153,6 @@ unexpected_successes = unexpected_successes + int(unexpected_success_count.group(1)) if error_count is not None: failures = failures + int(error_count.group(1)) - pass return passes, failures, unexpected_successes @@ -180,13 +161,74 @@ os.setpgid(os.getpid(), os.getpid()) +class CommunicatorThread(threading.Thread): + """Provides a thread class that communicates with a subprocess.""" + def __init__(self, process, event): + super(CommunicatorThread, self).__init__() + # Don't let this thread prevent shutdown. + self.daemon = True + self.process = process + self.pid = process.pid + self.event = event + self.output = None + + def run(self): + try: + # Communicate with the child process. + # This will not complete until the child process terminates. + self.output = self.process.communicate() + self.event.set() + except: + with output_lock: + print ("\nexception while using communicate() for " + "pid: {}").format(self.pid) + + +TIMEOUT_REGEX = re.compile(r"(^\d+)([smhd])?$") + + +def timeout_to_seconds(timeout): + """Converts timeout/gtimeout timeout values into seconds. + + @param timeout a timeout in the form of xm representing x minutes. + + @return None if timeout is None, or the number of seconds as a float + if a valid timeout format was specified. + """ + if timeout is None: + return None + else: + match = TIMEOUT_REGEX.match(timeout) + if match: + value = float(match.group(1)) + units = match.group(2) + if units is None: + # default is seconds. No conversion necessary. + return value + elif units == 's': + # Seconds. No conversion necessary. + return value + elif units == 'm': + # Value is in minutes. + return 60.0 * value + elif units == 'h': + # Value is in hours. + return (60.0 * 60.0) * value + elif units == 'd': + # Value is in days. + return 24 * (60.0 * 60.0) * value + else: + raise Exception("unexpected units value '{}'".format(units)) + else: + raise Exception("could not parse TIMEOUT spec '{}'".format( + timeout)) + + def call_with_timeout(command, timeout, name, inferior_pid_events): """Run command with a timeout if possible. -s QUIT will create a coredump if they are enabled on your system """ - process = None - if timeout_command and timeout != "0": - command = [timeout_command, '-s', 'QUIT', timeout] + command + timeout_seconds = timeout_to_seconds(timeout) if GET_WORKER_INDEX is not None: try: @@ -214,15 +256,68 @@ inferior_pid = process.pid if inferior_pid_events: inferior_pid_events.put_nowait(('created', inferior_pid)) - output = process.communicate() - # The inferior should now be entirely wrapped up. - exit_status = process.returncode - if exit_status is None: - raise Exception( - "no exit status available after the inferior dotest.py " - "should have completed") + # Create the synchronization event for notifying when the + # inferior dotest process is complete. + done_event = threading.Event() + done_event.clear() + + thread = CommunicatorThread(process, done_event) + thread.start() + + # Wait for process to end. + if timeout_seconds is not None and timeout_seconds > 0: + wait_result = done_event.wait(timeout_seconds) + if wait_result: + thread.join() + process.wait() + output = thread.output + exit_status = process.returncode + if exit_status is None: + raise Exception( + "no exit status available after the inferior dotest.py " + "should have completed") + else: + # The inferior dotest timed out. We could try to kill + # it gently, but there is little point and it complicates + # the cleanup. + process.kill() + + # Reap it so we don't leave a zombie around. + process.wait() + real_exit_status = process.returncode + + # The communication thread should go away on its own, but + # we'll warn if it doesn't. + thread.join(5.0) + if thread.isAlive(): + # TODO once we add a tap to the results event + # stream here, notify this condition via the event + # system. For now, print the error. + with output_lock: + print ("\n\nerror: failed to join communication thread " + "after killing timed-out pid {}".format( + inferior_pid)) + + output = ( + # Empty stdout + '', + # Add content for stderr. + 'timeout: exit code {}'.format(real_exit_status)) + + exit_status = eTimedOut + else: + # Wait indefinitely for the inferior dotest. + thread.join() + process.wait() + output = thread.output + exit_status = process.returncode + if exit_status is None: + raise Exception( + "no exit status available after the inferior dotest.py " + "should have completed") + # The inferior should now be entirely wrapped up. if inferior_pid_events: inferior_pid_events.put_nowait(('destroyed', inferior_pid)) @@ -232,10 +327,6 @@ # only stderr does. report_test_pass(name, output[1]) else: - # TODO need to differentiate a failing test from a run that - # was broken out of by a SIGTERM/SIGKILL, reporting those as - # an error. If a signal-based completion, need to call that - # an error. report_test_failure(name, command, output[1]) return name, exit_status, passes, failures, unexpected_successes Index: test/curses_results.py =================================================================== --- test/curses_results.py +++ test/curses_results.py @@ -219,7 +219,8 @@ self.status_panel.add_status_item(name="unexpected_success", title="Unexpected Success", format="%u", width=30, value=0, update=False) self.main_window.refresh() elif event == 'terminate': - self.main_window.key_event_loop() + # Greg - not sure why this is here. It locks up on exit. + # self.main_window.key_event_loop() lldbcurses.terminate_curses() check_for_one_key = False self.using_terminal = False
_______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org http://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits