summaryrefslogtreecommitdiffstats
path: root/tools/code_coverage
diff options
context:
space:
mode:
Diffstat (limited to 'tools/code_coverage')
-rwxr-xr-xtools/code_coverage/coverage_posix.py145
-rwxr-xr-xtools/code_coverage/coverage_posix_unittest.py72
2 files changed, 210 insertions, 7 deletions
diff --git a/tools/code_coverage/coverage_posix.py b/tools/code_coverage/coverage_posix.py
index 3015ca9..340199d 100755
--- a/tools/code_coverage/coverage_posix.py
+++ b/tools/code_coverage/coverage_posix.py
@@ -45,6 +45,9 @@ Linux:
RunPythonCommandInBuildDir() command to avoid the need for this
step.
+--timeout=SECS: if a subprocess doesn't have output within SECS,
+ assume it's a hang. Kill it and give up.
+
Strings after all options are considered tests to run. Test names
have all text before a ':' stripped to help with gyp compatibility.
For example, ../base/base.gyp:base_unittests is interpreted as a test
@@ -55,13 +58,113 @@ import glob
import logging
import optparse
import os
+import Queue
import shutil
+import signal
import subprocess
import sys
+import threading
import time
import traceback
+"""Global list of child PIDs to kill when we die."""
+gChildPIDs = []
+
+def TerminateSignalHandler(sig, stack):
+ """When killed, try and kill our child processes."""
+ signal.signal(sig, signal.SIG_DFL)
+ for pid in gChildPIDs:
+ if 'kill' in os.__all__: # POSIX
+ os.kill(pid, sig)
+ else:
+ subprocess.call(['taskkill.exe', '/PID', pid])
+ sys.exit(0)
+
+
+class RunTooLongException(Exception):
+ """Thrown when a command runs too long without output."""
+ pass
+
+
+class RunProgramThread(threading.Thread):
+ """A thread to run a subprocess.
+
+ We want to print the output of our subprocess in real time, but also
+ want a timeout if there has been no output for a certain amount of
+ time. Normal techniques (e.g. loop in select()) aren't cross
+ platform enough.
+ """
+ # Constants in our queue
+ LINE = 0
+ DIED = 1
+
+ def __init__(self, cmd):
+ super(RunProgramThread, self).__init__()
+ self._cmd = cmd
+ self._process = None
+ self._queue = Queue.Queue()
+ self._retcode = None
+
+ def run(self):
+ self._process = subprocess.Popen(self._cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT)
+ gChildPIDs.append(self._process.pid)
+ try:
+ while True:
+ line = self._process.stdout.readline()
+ if not line: # EOF
+ break
+ print line,
+ self._queue.put(RunProgramThread.LINE, True)
+ except IOError:
+ pass
+ # If we get here the process is dead.
+ gChildPIDs.remove(self._process.pid)
+ self._queue.put(RunProgramThread.DIED)
+
+ def stop(self):
+ self.kill()
+
+ def kill(self):
+ """Kill our running process if needed. Wait for kill to complete.
+
+ Should be called in the PARENT thread; we do not self-kill.
+ Returns the return code of the process.
+ Safe to call even if the process is dead."""
+ if not self._process:
+ return self._retcode
+ if 'kill' in os.__all__: # POSIX
+ os.kill(self._process.pid, signal.SIGTERM)
+ else:
+ subprocess.call(['taskkill.exe', '/PID', self._process.pid])
+ self._retcode = self._process.wait()
+ return self._retcode
+
+ def retcode(self):
+ """Return the return value of the subprocess.
+
+ Kill it if needed."""
+ return self.kill()
+
+ def RunUntilCompletion(self, timeout):
+ """Run thread until completion or timeout (in seconds).
+
+ Start the thread. Let it run until completion, or until we've
+ spent TIMEOUT without seeing output. On timeout throw
+ RunTooLongException."""
+ self.start()
+ while True:
+ try:
+ x = self._queue.get(True, timeout)
+ if x == RunProgramThread.DIED:
+ return self.retcode()
+ except Queue.Empty, e: # timed out
+ self.kill()
+ raise RunTooLongException()
+
+
class Coverage(object):
"""Doitall class for code coverage."""
@@ -189,9 +292,22 @@ class Coverage(object):
def Run(self, cmdlist, ignore_error=False, ignore_retcode=None,
explanation=None):
- """Run the command list; exit fatally on error."""
+ """Run the command list; exit fatally on error.
+
+ Args:
+ cmdlist: a list of commands (e.g. to pass to subprocess.call)
+ ignore_error: if True log an error; if False then exit.
+ ignore_retcode: if retcode is non-zero, exit unless we ignore.
+
+ Returns: process return code.
+ Throws: RunTooLongException if the process does not produce output
+ within TIMEOUT seconds; timeout is specified as a command line
+ option to the Coverage class and is set on init.
+ """
logging.info('Running ' + str(cmdlist))
- retcode = subprocess.call(cmdlist)
+ t = RunProgramThread(cmdlist)
+ retcode = t.RunUntilCompletion(self.options.timeout)
+
if retcode:
if ignore_error or retcode == ignore_retcode:
logging.warning('COVERAGE: %s unhappy but errors ignored %s' %
@@ -200,7 +316,7 @@ class Coverage(object):
logging.fatal('COVERAGE: %s failed; return code: %d' %
(str(cmdlist), retcode))
sys.exit(retcode)
-
+ return retcode
def IsPosix(self):
"""Return True if we are POSIX."""
@@ -417,10 +533,8 @@ class Coverage(object):
if self.options.strict:
sys.exit(retcode)
-def main():
- # Print out the args to help someone do it by hand if needed
- print >>sys.stderr, sys.argv
-
+def CoverageOptionParser():
+ """Return an optparse.OptionParser() suitable for Coverage object creation."""
parser = optparse.OptionParser()
parser.add_option('-d',
'--directory',
@@ -462,6 +576,23 @@ def main():
dest='xvfb',
default=True,
help='Use Xvfb for tests? Default True.')
+ parser.add_option('-T',
+ '--timeout',
+ dest='timeout',
+ default=9.9 * 60.0,
+ help='Timeout before bailing if a subprocess has no output.'
+ ' Default is a hair under 10min (Buildbot is 10min.)')
+ return parser
+
+
+def main():
+ # Print out the args to help someone do it by hand if needed
+ print >>sys.stderr, sys.argv
+
+ # Try and clean up nice if we're killed by buildbot
+ signal.signal(signal.SIGTERM, TerminateSignalHandler)
+
+ parser = CoverageOptionParser()
(options, args) = parser.parse_args()
if not options.directory:
parser.error('Directory not specified')
diff --git a/tools/code_coverage/coverage_posix_unittest.py b/tools/code_coverage/coverage_posix_unittest.py
new file mode 100755
index 0000000..e7ae155
--- /dev/null
+++ b/tools/code_coverage/coverage_posix_unittest.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+# Copyright (c) 2010 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Unit tests for coverage_posix.py."""
+
+import coverage_posix as coverage
+import sys
+import unittest
+
+class CoveragePosixTest(unittest.TestCase):
+
+ def setUp(self):
+ self.parseArgs()
+
+ def parseArgs(self):
+ """Setup and process arg parsing."""
+ self.parser = coverage.CoverageOptionParser()
+ (self.options, self.args) = self.parser.parse_args()
+
+ def testSanity(self):
+ """Sanity check we're able to actually run the tests.
+
+ Simply creating a Coverage instance checks a few things (e.g. on
+ Windows that the coverage tools can be found)."""
+ c = coverage.Coverage('.', self.options, self.args)
+
+ def testRunBasicProcess(self):
+ """Test a simple run of a subprocess."""
+ c = coverage.Coverage('.', self.options, self.args)
+ for code in range(2):
+ retcode = c.Run([sys.executable, '-u', '-c',
+ 'import sys; sys.exit(%d)' % code],
+ ignore_error=True)
+ self.assertEqual(code, retcode)
+
+ def testRunSlowProcess(self):
+ """Test program which prints slowly but doesn't hit our timeout.
+
+ Overall runtime is longer than the timeout but output lines
+ trickle in keeping things alive.
+ """
+ self.options.timeout = 2.5
+ c = coverage.Coverage('.', self.options, self.args)
+ slowscript = ('import sys, time\n'
+ 'for x in range(10):\n'
+ ' time.sleep(0.5)\n'
+ ' print "hi mom"\n'
+ 'sys.exit(0)\n')
+ retcode = c.Run([sys.executable, '-u', '-c', slowscript])
+ self.assertEqual(0, retcode)
+
+ def testRunExcessivelySlowProcess(self):
+ """Test program which DOES hit our timeout.
+
+ Initial lines should print but quickly it takes too long and
+ should be killed.
+ """
+ self.options.timeout = 2.5
+ c = coverage.Coverage('.', self.options, self.args)
+ slowscript = ('import time\n'
+ 'for x in range(1,10):\n'
+ ' print "sleeping for %d" % x\n'
+ ' time.sleep(x)\n')
+ self.assertRaises(Exception,
+ c.Run,
+ [sys.executable, '-u', '-c', slowscript])
+
+
+if __name__ == '__main__':
+ unittest.main()