changeset 302:fe966b950424

* Add a `<c:gcov>` command based on patch by Chandler Carruth. Closes #72. * Enable more extensive unit testing of recipe commands by providing a simple ''dummy'' implementation of the `CommandLine` class.
author cmlenz
date Mon, 07 Nov 2005 17:58:22 +0000
parents d486e34084af
children 3d58d9dd11c8
files bitten/build/api.py bitten/build/ctools.py bitten/build/tests/ctools.py bitten/build/tests/dummy.py setup.py
diffstat 5 files changed, 178 insertions(+), 19 deletions(-) [+]
line wrap: on
line diff
--- a/bitten/build/api.py
+++ b/bitten/build/api.py
@@ -24,6 +24,23 @@
     """Exception raised when the execution of a command times out."""
 
 
+def _combine(*iterables):
+    iterables = [iter(iterable) for iterable in iterables]
+    size = len(iterables)
+    while True:
+        to_yield = [None] * size
+        for idx, iterable in enumerate(iterables):
+            if iterable is None:
+                continue
+            try:
+                to_yield[idx] = iterable.next()
+            except StopIteration:
+                iterables[idx] = None
+        if not [iterable for iterable in iterables if iterable is not None]:
+            break
+        yield tuple(to_yield)
+
+
 class CommandLine(object):
     """Simple helper for executing subprocesses."""
     # TODO: Use 'subprocess' module if available (Python >= 2.4)
@@ -102,7 +119,7 @@
                 if self.cwd:
                     os.chdir(old_cwd)
 
-            for out_line, err_line in self._combine(out_lines, err_lines):
+            for out_line, err_line in _combine(out_lines, err_lines):
                 yield out_line and out_line.rstrip(), \
                       err_line and err_line.rstrip()
 
@@ -157,7 +174,7 @@
                         err_eof = True
                 out_lines = self._extract_lines(out_data)
                 err_lines = self._extract_lines(err_data)
-                for out_line, err_line in self._combine(out_lines, err_lines):
+                for out_line, err_line in _combine(out_lines, err_lines):
                     yield out_line, err_line
                 time.sleep(.1)
             self.returncode = pipe.wait()
@@ -167,22 +184,6 @@
             if self.cwd:
                 os.chdir(old_cwd)
 
-    def _combine(self, *iterables):
-        iterables = [iter(iterable) for iterable in iterables]
-        size = len(iterables)
-        while True:
-            to_yield = [None] * size
-            for idx, iterable in enumerate(iterables):
-                if iterable is None:
-                    continue
-                try:
-                    to_yield[idx] = iterable.next()
-                except StopIteration:
-                    iterables[idx] = None
-            if not [iterable for iterable in iterables if iterable is not None]:
-                break
-            yield tuple(to_yield)
-
     def _extract_lines(self, data):
         extracted = []
         def _endswith_linesep(string):
--- a/bitten/build/ctools.py
+++ b/bitten/build/ctools.py
@@ -8,8 +8,14 @@
 # are also available at http://bitten.cmlenz.net/wiki/License.
 
 import logging
+import re
+import os
+try:
+    set
+except NameError:
+    from sets import Set as set
 
-from bitten.build import CommandLine
+from bitten.build import CommandLine, FileSet
 from bitten.util import xmlio
 
 log = logging.getLogger('bitten.build.ctools')
@@ -118,3 +124,69 @@
     except xmlio.ParseError, e:
         print e
         log.warning('Error parsing CppUnit results file (%s)', e)
+
+def gcov(ctxt, include=None, exclude=None, prefix=None):
+    """Run `gcov` to extract coverage data where available."""
+    file_re = re.compile(r'^File \`(?P<file>[^\']+)\'\s*$')
+    lines_re = re.compile(r'^Lines executed:(?P<cov>\d+\.\d+)\% of (?P<num>\d+)\s*$')
+
+    files = []
+    for filename in FileSet(ctxt.basedir, include, exclude):
+        if os.path.splitext(filename)[1] in ('.c', '.cpp', '.cc', '.cxx'):
+            files.append(filename)
+
+    coverage = xmlio.Fragment()
+
+    for srcfile in files:
+        # Determine the coverage for each source file by looking for a .gcno
+        # and .gcda pair
+        filepath, filename = os.path.split(srcfile)
+        stem = os.path.splitext(filename)[0]
+        if prefix is not None:
+            stem = prefix + '-' + stem
+
+        objfile = os.path.join(filepath, stem + '.o')
+        if not os.path.isfile(ctxt.resolve(objfile)):
+            log.warn('No object file found for %s at %s', srcfile, objfile)
+            continue
+        if not os.path.isfile(ctxt.resolve(stem + '.gcno')):
+            log.warn('No .gcno file found for %s', srcfile)
+            continue
+        if not os.path.isfile(ctxt.resolve(stem + '.gcda')):
+            log.warn('No .gcda file found for %s', srcfile)
+            continue
+
+        num_lines, num_covered = 0, 0
+        skip_block = False
+        cmd = CommandLine('gcov', ['-b', '-n', '-o', objfile, srcfile],
+                          cwd=ctxt.basedir)
+        for out, err in cmd.execute():
+            if out == '': # catch blank lines, reset the block state...
+                skip_block = False
+            elif out and not skip_block:
+                # Check for a file name
+                match = file_re.match(out)
+                if match:
+                    if os.path.isabs(match.group('file')):
+                        skip_block = True
+                        continue
+                else:
+                    # check for a "Lines executed" message
+                    match = lines_re.match(out)
+                    if match:
+                        lines = float(match.group('num'))
+                        cov = float(match.group('cov'))
+                        num_covered += int(lines * cov / 100)
+                        num_lines += int(lines)
+        if cmd.returncode != 0:
+            continue
+
+        module = xmlio.Element('coverage', name=os.path.basename(srcfile),
+                                file=srcfile.replace(os.sep, '/'),
+                                lines=num_lines, percentage=0)
+        if num_lines:
+            percent = int(round(num_covered * 100 / num_lines))
+            module.attr['percentage'] = percent
+        coverage.append(module)
+
+    ctxt.report('coverage', coverage)
--- a/bitten/build/tests/ctools.py
+++ b/bitten/build/tests/ctools.py
@@ -13,6 +13,7 @@
 import unittest
 
 from bitten.build import ctools
+from bitten.build.tests import dummy
 from bitten.recipe import Context, Recipe
 
 
@@ -83,9 +84,67 @@
         self.assertEqual('success', tests[2].attr['status'])
 
 
+class GCovTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.basedir = os.path.realpath(tempfile.mkdtemp())
+        self.ctxt = Context(self.basedir)
+
+    def tearDown(self):
+        shutil.rmtree(self.basedir)
+
+    def _create_file(self, *path):
+        filename = os.path.join(self.basedir, *path)
+        dirname = os.path.dirname(filename)
+        if not os.path.isdir(dirname):
+            os.makedirs(dirname)
+        fd = file(filename, 'w')
+        fd.close()
+        return filename[len(self.basedir) + 1:]
+
+    def test_no_file(self):
+        ctools.CommandLine = dummy.CommandLine()
+        ctools.gcov(self.ctxt)
+        type, category, generator, xml = self.ctxt.output.pop()
+        self.assertEqual('report', type)
+        self.assertEqual('coverage', category)
+        self.assertEqual(0, len(xml.children))
+
+    def test_single_file(self):
+        self._create_file('foo.c')
+        self._create_file('foo.o')
+        self._create_file('foo.gcno')
+        self._create_file('foo.gcda')
+
+        ctools.CommandLine = dummy.CommandLine(stdout="""
+File `foo.c'
+Lines executed:45.81% of 884
+Branches executed:54.27% of 398
+Taken at least once:36.68% of 398
+Calls executed:48.19% of 249
+
+File `foo.h'
+Lines executed:50.00% of 4
+No branches
+Calls executed:100.00% of 1
+""")
+        ctools.gcov(self.ctxt)
+        type, category, generator, xml = self.ctxt.output.pop()
+        self.assertEqual('report', type)
+        self.assertEqual('coverage', category)
+        self.assertEqual(1, len(xml.children))
+        elem = xml.children[0]
+        self.assertEqual('coverage', elem.name)
+        self.assertEqual('foo.c', elem.attr['file'])
+        self.assertEqual('foo.c', elem.attr['name'])
+        self.assertEqual(888, elem.attr['lines'])
+        self.assertEqual(45, elem.attr['percentage'])
+
+
 def suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(CppUnitTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(GCovTestCase, 'test'))
     return suite
 
 if __name__ == '__main__':
new file mode 100644
--- /dev/null
+++ b/bitten/build/tests/dummy.py
@@ -0,0 +1,26 @@
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://bitten.cmlenz.net/wiki/License.
+
+from StringIO import StringIO
+
+from bitten.build import api
+
+
+class CommandLine(api.CommandLine):
+
+    def __init__(self, returncode=0, stdout='', stderr=''):
+        self.returncode = returncode
+        self.stdout = StringIO(stdout)
+        self.stderr = StringIO(stderr)
+
+    def __call__(self, executable, args, input=None, cwd=None):
+        return self
+
+    def execute(self):
+        return api._combine(self.stdout.readlines(), self.stderr.readlines())
--- a/setup.py
+++ b/setup.py
@@ -46,6 +46,7 @@
             NS + 'sh#pipe = bitten.build.shtools:pipe',
             NS + 'c#configure = bitten.build.ctools:configure',
             NS + 'c#cppunit = bitten.build.ctools:cppunit',
+            NS + 'c#gcov = bitten.build.ctools:gcov',
             NS + 'c#make = bitten.build.ctools:make',
             NS + 'java#ant = bitten.build.javatools:ant',
             NS + 'java#junit = bitten.build.javatools:junit',
Copyright (C) 2012-2017 Edgewall Software