run-tests: switch timeout handling from alarm to helper thread
authorMatt Mackall <mpm@selenic.com>
Sun, 24 Apr 2011 16:42:15 -0500
changeset 14001 9c4da6ab4e5a
parent 14000 636a6f5aa2cd
child 14002 a738c30d4b18
run-tests: switch timeout handling from alarm to helper thread This should be slightly more portable than signals and be compatible with threaded dispatch.
tests/run-tests.py
--- a/tests/run-tests.py	Sun Apr 24 16:42:11 2011 -0500
+++ b/tests/run-tests.py	Sun Apr 24 16:42:15 2011 -0500
@@ -56,14 +56,29 @@
 import threading
 
 closefds = os.name == 'posix'
-def Popen4(cmd, bufsize=-1):
-    p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
+def Popen4(cmd, timeout):
+    p = subprocess.Popen(cmd, shell=True, bufsize=-1,
                          close_fds=closefds,
                          stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                          stderr=subprocess.STDOUT)
     p.fromchild = p.stdout
     p.tochild = p.stdin
     p.childerr = p.stderr
+
+    if timeout:
+        p.timeout = False
+        def t():
+            start = time.time()
+            while time.time() - start < timeout and p.returncode is None:
+                time.sleep(1)
+            p.timeout = True
+            if p.returncode is None:
+                try:
+                    p.terminate()
+                except OSError:
+                    pass
+        threading.Thread(target=t).start()
+
     return p
 
 # reserved exit code to skip test (used by hghave)
@@ -440,12 +455,6 @@
             os.mkdir(adir)
         covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
 
-class Timeout(Exception):
-    pass
-
-def alarmed(signum, frame):
-    raise Timeout
-
 def pytest(test, options, replacements):
     py3kswitch = options.py3k_warnings and ' -3' or ''
     cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test)
@@ -603,33 +612,38 @@
         if ret is None:
             ret = 0
     else:
-        proc = Popen4(cmd)
+        proc = Popen4(cmd, options.timeout)
         def cleanup():
-            os.kill(proc.pid, signal.SIGTERM)
+            try:
+                proc.terminate()
+            except OSError:
+                pass
             ret = proc.wait()
             if ret == 0:
                 ret = signal.SIGTERM << 8
             killdaemons()
             return ret
 
+        output = ''
+        proc.tochild.close()
+
         try:
-            output = ''
-            proc.tochild.close()
             output = proc.fromchild.read()
-            ret = proc.wait()
-            if wifexited(ret):
-                ret = os.WEXITSTATUS(ret)
-        except Timeout:
-            vlog('# Process %d timed out - killing it' % proc.pid)
-            cleanup()
-            ret = 'timeout'
-            output += ("\n### Abort: timeout after %d seconds.\n"
-                       % options.timeout)
         except KeyboardInterrupt:
             vlog('# Handling keyboard interrupt')
             cleanup()
             raise
 
+        ret = proc.wait()
+        if wifexited(ret):
+            ret = os.WEXITSTATUS(ret)
+
+        if proc.timeout:
+            ret = 'timeout'
+
+        if ret:
+            killdaemons()
+
     for s, r in replacements:
         output = re.sub(s, r, output)
     return ret, splitnewlines(output)
@@ -755,9 +769,6 @@
     os.mkdir(testtmp)
     os.chdir(testtmp)
 
-    if options.timeout > 0:
-        signal.alarm(options.timeout)
-
     ret, out = runner(testpath, options, [
         (re.escape(testtmp), '$TESTTMP'),
         (r':%s\b' % options.port, ':$HGPORT'),
@@ -766,9 +777,6 @@
         ])
     vlog("# Ret was:", ret)
 
-    if options.timeout > 0:
-        signal.alarm(0)
-
     mark = '.'
     if ret == 0:
         success()
@@ -807,11 +815,12 @@
             skipped = False
         else:
             skip(missing[-1])
+    elif ret == 'timeout':
+        mark = 't'
+        fail("timed out", ret)
     elif out != refout:
         mark = '!'
-        if ret == 'timeout':
-            fail("timed out", ret)
-        elif ret:
+        if ret:
             fail("output changed and returned error code %d" % ret, ret)
         else:
             fail("output changed", ret)
@@ -962,15 +971,6 @@
             installhg(options)
             _checkhglib("Testing")
 
-        if options.timeout > 0:
-            try:
-                signal.signal(signal.SIGALRM, alarmed)
-                vlog('# Running each test with %d second timeout' %
-                     options.timeout)
-            except AttributeError:
-                print 'WARNING: cannot run tests with timeouts'
-                options.timeout = 0
-
         if options.restart:
             orig = list(tests)
             while tests: