tests/run-tests.py
changeset 917 6f25a5834e14
parent 916 dd5c50a018b0
child 918 3071ec5d085c
--- a/tests/run-tests.py	Mon Apr 25 01:42:21 2011 +0100
+++ b/tests/run-tests.py	Mon Apr 25 23:09:47 2011 +0100
@@ -53,16 +53,32 @@
 import tempfile
 import time
 import re
+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)
@@ -439,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)
@@ -602,72 +612,90 @@
         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)
 
-def runone(options, test, results):
+def runone(options, test):
     '''tristate output:
     None -> skipped
     True -> passed
     False -> failed'''
 
+    global results, resultslock, iolock
+
     testpath = os.path.join(TESTDIR, test)
 
+    def result(l, e):
+        resultslock.acquire()
+        results[l].append(e)
+        resultslock.release()
+
     def skip(msg):
         if not options.verbose:
-            results['s'].append((test, msg))
+            result('s', (test, msg))
         else:
+            iolock.acquire()
             print "\nSkipping %s: %s" % (testpath, msg)
+            iolock.release()
         return None
 
     def fail(msg, ret):
         if not options.nodiff:
+            iolock.acquire()
             print "\nERROR: %s %s" % (testpath, msg)
+            iolock.release()
         if (not ret and options.interactive
             and os.path.exists(testpath + ".err")):
+            iolock.acquire()
             print "Accept this change? [n] ",
             answer = sys.stdin.readline().strip()
+            iolock.release()
             if answer.lower() in "y yes".split():
                 if test.endswith(".t"):
                     rename(testpath + ".err", testpath)
                 else:
                     rename(testpath + ".err", testpath + ".out")
                 return
-        results['f'].append((test, msg))
+        result('f', (test, msg))
 
     def success():
-        results['p'].append(test)
+        result('p', test)
 
     def ignore(msg):
-        results['i'].append((test, msg))
+        result('i', (test, msg))
 
     if (test.startswith("test-") and '~' not in test and
         ('.' not in test or test.endswith('.py') or
@@ -681,7 +709,7 @@
     if options.blacklist:
         filename = options.blacklist.get(test)
         if filename is not None:
-            skipped.append((test, "blacklisted (%s)" % filename))
+            skip("blacklisted")
             return None
 
     if options.retest and not os.path.exists(test + ".err"):
@@ -747,9 +775,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'),
@@ -758,9 +783,6 @@
         ])
     vlog("# Ret was:", ret)
 
-    if options.timeout > 0:
-        signal.alarm(0)
-
     mark = '.'
     if ret == 0:
         success()
@@ -799,27 +821,32 @@
             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:
-            fail("output changed and returned error code %d" % ret, ret)
-        else:
-            fail("output changed", ret)
-        if ret != 'timeout' and not options.nodiff:
+        if not options.nodiff:
+            iolock.acquire()
             if options.view:
                 os.system("%s %s %s" % (options.view, ref, err))
             else:
                 showdiff(refout, out, ref, err)
+            iolock.release()
+        if ret:
+            fail("output changed and returned error code %d" % ret, ret)
+        else:
+            fail("output changed", ret)
         ret = 1
     elif ret:
         mark = '!'
         fail("returned error code %d" % ret, ret)
 
     if not options.verbose:
+        iolock.acquire()
         sys.stdout.write(mark)
         sys.stdout.flush()
+        iolock.release()
 
     killdaemons()
 
@@ -935,9 +962,13 @@
         outputcoverage(options)
     sys.exit(failures != 0)
 
+results = dict(p=[], f=[], s=[], i=[])
+resultslock = threading.Lock()
+iolock = threading.Lock()
+
 def runqueue(options, tests, results):
     for test in tests:
-        ret = runone(options, test, results)
+        ret = runone(options, test)
         if options.first and ret is not None and not ret:
             break
 
@@ -946,22 +977,11 @@
     DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
     HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
 
-    results = dict(p=[], f=[], s=[], i=[])
-
     try:
         if INST:
             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: