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,9 +284,14 @@ cmd = ShUtil.Seq(cmd, '&&', c) results = [] + wd = None try: + if litConfig.timeout is not None: + wd = lit.util.Watchdog(litConfig.timeout) shenv = ShellEnvironment(cwd, test.config.environment) - exitCode = executeShCmd(cmd, shenv, results) + exitCode = executeShCmd(cmd, shenv, results, wd) + if wd is not None: + wd.cancel() except InternalShellError: e = sys.exc_info()[1] exitCode = 127 @@ -296,6 +304,9 @@ out += 'Command %d Output:\n%s\n\n' % (i, cmd_out) out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err) + if wd is not None and wd.timed_out: + out += "\n\nTimed out after %.2f seconds" % (litConfig.timeout) + return out, err, exitCode def executeScript(test, litConfig, tmpBase, commands, cwd): @@ -332,7 +343,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,7 +6,46 @@ import signal import subprocess import sys +import threading +class Watchdog(object): + """ + Watches a Popen call, and kills it after a timeout. + """ + def __init__(self, timeout): + self.popens = [] + self.timed_out = False + self.timer = threading.Timer(timeout, self.handler) + self.timer.start() + + def _safely_kill(self, popen): + if popen.poll() is None: + try: + popen.kill() + 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): + self.timed_out = True + for popen in self.popens: + self._safely_kill(popen) + + def watch(self, popen): + self.popens.append(popen) + + # Take care of the race condition where the timer timed out before the + # popen was added to the list. + if self.timed_out: + self._safely_kill(popen) + + def cancel(self): + if self.timer is not None: + self.timer.cancel() + + def to_bytes(str): # Encode to UTF-8 to get binary data. return str.encode('utf-8') @@ -158,15 +197,25 @@ # 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) + + if timeout is not None: + wd = Watchdog(timeout) + wd.watch(p) + out,err = p.communicate() exitCode = p.wait() + if timeout is not None: + wd.cancel() + if wd.timed_out: + err += "\n\nTimed out after %.2f seconds" % (timeout) + # Detect Ctrl-C in subprocess. if exitCode == -signal.SIGINT: raise KeyboardInterrupt 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