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
[email protected]
http://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits