Index: utils/lit/lit/LitConfig.py =================================================================== --- utils/lit/lit/LitConfig.py +++ utils/lit/lit/LitConfig.py @@ -21,7 +21,7 @@ def __init__(self, progname, path, quiet, useValgrind, valgrindLeakCheck, valgrindArgs, noExecute, debug, isWindows, - params, config_prefix = None): + params, config_prefix = None, timeout = None): # The name of the test runner. self.progname = progname # The items to add to the PATH environment variable. @@ -57,6 +57,7 @@ self.valgrindArgs.append('--leak-check=no') self.valgrindArgs.extend(self.valgrindUserArgs) + self.timeout = timeout def load_config(self, config, path): """load_config(config, path) - Load a config object from an alternate Index: utils/lit/lit/TestRunner.py =================================================================== --- utils/lit/lit/TestRunner.py +++ utils/lit/lit/TestRunner.py @@ -33,28 +33,28 @@ self.cwd = cwd self.env = env -def executeShCmd(cmd, shenv, results): +def executeShCmd(cmd, shenv, results, watchdog=None): if isinstance(cmd, ShUtil.Seq): if cmd.op == ';': - res = executeShCmd(cmd.lhs, shenv, results) - return executeShCmd(cmd.rhs, shenv, results) + res = executeShCmd(cmd.lhs, shenv, results, watchdog) + return executeShCmd(cmd.rhs, shenv, results, watchdog) if cmd.op == '&': raise InternalShellError(cmd,"unsupported shell operator: '&'") if cmd.op == '||': - res = executeShCmd(cmd.lhs, shenv, results) + res = executeShCmd(cmd.lhs, shenv, results, watchdog) if res != 0: - res = executeShCmd(cmd.rhs, shenv, results) + res = executeShCmd(cmd.rhs, shenv, results, watchdog) return res if cmd.op == '&&': - res = executeShCmd(cmd.lhs, shenv, results) + res = executeShCmd(cmd.lhs, shenv, results, watchdog) if res is None: return res if res == 0: - res = executeShCmd(cmd.rhs, shenv, results) + res = executeShCmd(cmd.rhs, shenv, results, watchdog) return res raise ValueError('Unknown shell command: %r' % cmd.op) @@ -171,13 +171,16 @@ args[i] = f.name try: - procs.append(subprocess.Popen(args, cwd=shenv.cwd, - executable = executable, - stdin = stdin, - stdout = stdout, - stderr = stderr, - env = shenv.env, - close_fds = kUseCloseFDs)) + p = subprocess.Popen(args, cwd=shenv.cwd, + executable = executable, + stdin = stdin, + stdout = stdout, + stderr = stderr, + env = shenv.env, + close_fds = kUseCloseFDs) + procs.append(p) + if watchdog is not None: + watchdog.watch(p) except OSError as e: raise InternalShellError(j, 'Could not create process due to {}'.format(e)) @@ -281,20 +284,24 @@ cmd = ShUtil.Seq(cmd, '&&', c) results = [] - try: - shenv = ShellEnvironment(cwd, test.config.environment) - exitCode = executeShCmd(cmd, shenv, results) - except InternalShellError: - e = sys.exc_info()[1] - exitCode = 127 - results.append((e.command, '', e.message, exitCode)) - - out = err = '' - for i,(cmd, cmd_out,cmd_err,res) in enumerate(results): - out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args)) - out += 'Command %d Result: %r\n' % (i, res) - out += 'Command %d Output:\n%s\n\n' % (i, cmd_out) - out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err) + with lit.util.Watchdog(litConfig.timeout) as wd: + try: + shenv = ShellEnvironment(cwd, test.config.environment) + exitCode = executeShCmd(cmd, shenv, results, wd) + except InternalShellError: + e = sys.exc_info()[1] + exitCode = 127 + results.append((e.command, '', e.message, exitCode)) + + out = err = '' + for i,(cmd, cmd_out,cmd_err,res) in enumerate(results): + out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args)) + out += 'Command %d Result: %r\n' % (i, res) + out += 'Command %d Output:\n%s\n\n' % (i, cmd_out) + out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err) + + if wd.timed_out(): + out += "\n\nTimed out after %.2f seconds" % (litConfig.timeout) return out, err, exitCode @@ -332,7 +339,8 @@ command = litConfig.valgrindArgs + command return lit.util.executeCommand(command, cwd=cwd, - env=test.config.environment) + env=test.config.environment, + timeout=litConfig.timeout) def parseIntegratedTestScriptCommands(source_path): """ Index: utils/lit/lit/formats/base.py =================================================================== --- utils/lit/lit/formats/base.py +++ utils/lit/lit/formats/base.py @@ -101,7 +101,7 @@ else: cmd.append(test.getSourcePath()) - out, err, exitCode = lit.util.executeCommand(cmd) + out, err, exitCode = lit.util.executeCommand(cmd, timeout=litConfig.timeout) diags = out + err if not exitCode and not diags.strip(): Index: utils/lit/lit/main.py =================================================================== --- utils/lit/lit/main.py +++ utils/lit/lit/main.py @@ -208,6 +208,9 @@ group.add_option("", "--max-time", dest="maxTime", metavar="N", help="Maximum time to spend testing (in seconds)", action="store", type=float, default=None) + group.add_option("", "--timeout", dest="timeout", metavar="N", + help="Maximum time to spend in any given test (in seconds)", + action="store", type=float, default=None) group.add_option("", "--shuffle", dest="shuffle", help="Run tests in random order", action="store_true", default=False) @@ -281,7 +284,8 @@ debug = opts.debug, isWindows = isWindows, params = userParams, - config_prefix = opts.configPrefix) + config_prefix = opts.configPrefix, + timeout = opts.timeout) # Perform test discovery. run = lit.run.Run(litConfig, Index: utils/lit/lit/util.py =================================================================== --- utils/lit/lit/util.py +++ utils/lit/lit/util.py @@ -6,6 +6,81 @@ import signal import subprocess import sys +import threading + +class Watchdog(object): + """ + Watches one or more Popen calls, and kills them after a timeout. + + Timing starts when the first watch call has been made. + + A timeout is said to have occurred if the timer runs out before cancel() is + called (regardless of whether there were any popens that needed killing). + """ + def __init__(self, timeout, popen=None): + self._lock = threading.Lock() + self._popens = [] + self._timed_out = False + self._started = False + + if timeout is not None: + self._timer = threading.Timer(timeout, self._handler) + else: + self._timer = None + + if popen is not None: + self.watch(popen) + + def _safely_kill(self, popen): + if popen.poll() is None: + try: + os.kill(popen.pid, signal.SIGTERM) + except OSError, e: + # The popen terminated or was killed by something else between + # the test above and when kill was called... either way we got + # what we wanted. + pass + + def _handler(self): + with self._lock: + self._timed_out = True + for popen in self._popens: + self._safely_kill(popen) + + def watch(self, popen): + with self._lock: + if self._timed_out: + # Take care of the race condition where the timer timed out + # before the popen was added to the list. + self._safely_kill(popen) + else: + self._popens.append(popen) + if not self._started and self._timer is not None: + self._started = True + self._timer.start() + + def kill(self): + self._handler() + + def timed_out(self): + """Query whether a timeout has occurred""" + with self._lock: + return self._started and self._timed_out + + def cancel(self): + """Call off the watchdog, canceling its timer""" + with self._lock: + if self._timer is not None: + self._timer.cancel() + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + if value is not None: + self.kill() + self.cancel() + def to_bytes(str): # Encode to UTF-8 to get binary data. @@ -158,14 +233,18 @@ # Close extra file handles on UNIX (on Windows this cannot be done while # also redirecting input). kUseCloseFDs = not (platform.system() == 'Windows') -def executeCommand(command, cwd=None, env=None): +def executeCommand(command, cwd=None, env=None, timeout=None): p = subprocess.Popen(command, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, close_fds=kUseCloseFDs) - out,err = p.communicate() - exitCode = p.wait() + + with Watchdog(timeout, p) as wd: + out,err = p.communicate() + exitCode = p.wait() + if wd.timed_out(): + err += "\n\nTimed out after %.2f seconds" % (timeout) # Detect Ctrl-C in subprocess. if exitCode == -signal.SIGINT: Index: utils/lit/tests/Inputs/timeout/infloop.py =================================================================== --- utils/lit/tests/Inputs/timeout/infloop.py +++ utils/lit/tests/Inputs/timeout/infloop.py @@ -0,0 +1,8 @@ +# RUN: %{python} %s +# XFAIL: * + +import sys + +print "infinite loop" +while True: + pass Index: utils/lit/tests/Inputs/timeout/lit.cfg =================================================================== --- utils/lit/tests/Inputs/timeout/lit.cfg +++ utils/lit/tests/Inputs/timeout/lit.cfg @@ -0,0 +1,16 @@ +# -*- Python -*- + +import os +import sys + +import lit.formats + +config.name = 'timeout' +config.test_format = lit.formats.ShTest(execute_external=False) +config.suffixes = ['.py'] +config.test_source_root = os.path.dirname(__file__) +config.test_exec_root = config.test_source_root +config.target_triple = '(unused)' +src_root = os.path.join(config.test_source_root, '..') +config.environment['PYTHONPATH'] = src_root +config.substitutions.append(('%{python}', sys.executable)) Index: utils/lit/tests/Inputs/timeout/short.py =================================================================== --- utils/lit/tests/Inputs/timeout/short.py +++ utils/lit/tests/Inputs/timeout/short.py @@ -0,0 +1,5 @@ +# RUN: %{python} %s + +import sys + +print "short program" Index: utils/lit/tests/timeout.py =================================================================== --- utils/lit/tests/timeout.py +++ utils/lit/tests/timeout.py @@ -0,0 +1,9 @@ +# RUN: %{lit} \ +# RUN: %{inputs}/timeout/infloop.py \ +# RUN: %{inputs}/timeout/short.py \ +# RUN: -j 1 -v --debug --timeout 0.1 > %t.out 2> %t.err +# RUN: FileCheck --check-prefix=CHECK-OUT < %t.out %s +# + +# CHECK-OUT: XFAIL: timeout :: infloop.py +# CHECK-OUT: PASS: timeout :: short.py