changeset 482:b87eda443ffc

add figleaf coverage support
author mgood
date Mon, 17 Mar 2008 23:25:16 +0000
parents 0ec997423fce
children 9514fad39d60
files bitten/build/pythontools.py bitten/build/tests/pythontools.py bitten/util/testrunner.py setup.py
diffstat 4 files changed, 203 insertions(+), 4 deletions(-) [+]
line wrap: on
line diff
--- a/bitten/build/pythontools.py
+++ b/bitten/build/pythontools.py
@@ -1,7 +1,8 @@
 # -*- coding: utf-8 -*-
 #
 # Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
-# Copyright (C) 2007 Edgewall Software
+# Copyright (C) 2008 Matt Good <matt@matt-good.net>
+# Copyright (C) 2008 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -10,8 +11,11 @@
 
 """Recipe commands for tools commonly used by Python projects."""
 
+from __future__ import division
+
 import logging
 import os
+import cPickle as pickle
 import re
 try:
     set
@@ -368,6 +372,55 @@
     except IOError, e:
         log.warning('Error opening coverage summary file (%s)', e)
 
+def figleaf(ctxt, summary=None, include=None, exclude=None):
+    from figleaf import get_lines
+    coverage = xmlio.Fragment()
+    try:
+        fileobj = open(ctxt.resolve(summary))
+    except IOError, e:
+        log.warning('Error opening coverage summary file (%s)', e)
+        return
+    coverage_data = pickle.load(fileobj)
+    fileset = FileSet(ctxt.basedir, include, exclude)
+    for filename in fileset:
+        base, ext = os.path.splitext(filename)
+        if ext != '.py':
+            continue
+        modname = base.replace('/', '.')
+        realfilename = ctxt.resolve(filename)
+        interesting_lines = get_lines(open(realfilename))
+        covered_lines = coverage_data.get(realfilename, set())
+        percentage = int(round(len(covered_lines) * 100 / len(interesting_lines)))
+        line_hits = []
+        for lineno in xrange(1, max(interesting_lines)+1):
+            if lineno not in interesting_lines:
+                line_hits.append('-')
+            elif lineno in covered_lines:
+                line_hits.append('1')
+            else:
+                line_hits.append('0')
+        module = xmlio.Element('coverage', name=modname,
+                               file=filename,
+                               percentage=percentage,
+                               lines=len(interesting_lines),
+                               line_hits=' '.join(line_hits))
+        coverage.append(module)
+    ctxt.report('coverage', coverage)
+
+def _normalize_filenames(ctxt, filenames, fileset):
+    for filename in filenames:
+        if not os.path.isabs(filename):
+            filename = os.path.normpath(os.path.join(ctxt.basedir,
+                                                     filename))
+        else:
+            filename = os.path.realpath(filename)
+        if not filename.startswith(ctxt.basedir):
+            continue
+        filename = filename[len(ctxt.basedir) + 1:]
+        if filename not in fileset:
+            continue
+        yield filename.replace(os.sep, '/')
+
 def unittest(ctxt, file_=None):
     """Extract data from a unittest results file in XML format.
     
--- a/bitten/build/tests/pythontools.py
+++ b/bitten/build/tests/pythontools.py
@@ -1,7 +1,8 @@
 # -*- coding: utf-8 -*-
 #
 # Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
-# Copyright (C) 2007 Edgewall Software
+# Copyright (C) 2008 Matt Good <matt@matt-good.net>
+# Copyright (C) 2008 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -9,11 +10,13 @@
 # are also available at http://bitten.edgewall.org/wiki/License.
 
 import os
+import cPickle as pickle
 import shutil
 import tempfile
 import unittest
 
 from bitten.build import pythontools
+from bitten.build import FileSet
 from bitten.recipe import Context, Recipe
 
 
@@ -197,6 +200,134 @@
         self.assertEqual('test/module.py', child.attr['file'])
 
 
+class FigleafTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.basedir = os.path.realpath(tempfile.mkdtemp())
+        self.ctxt = Context(self.basedir)
+        self.summary = open(os.path.join(self.basedir, '.figleaf'), 'w')
+
+    def tearDown(self):
+        shutil.rmtree(self.basedir)
+
+    def _create_file(self, *path):
+        filename = os.path.join(self.basedir, *path)
+        dirname = os.path.dirname(filename)
+        os.makedirs(dirname)
+        fd = file(filename, 'w')
+        fd.close()
+        return filename[len(self.basedir) + 1:]
+
+    def test_missing_param_summary(self):
+        self.summary.close()
+        self.assertRaises(AssertionError, pythontools.coverage, self.ctxt)
+
+    def test_empty_summary(self):
+        pickle.dump({}, self.summary)
+        self.summary.close()
+        pythontools.figleaf(self.ctxt, summary=self.summary.name, include='*.py')
+        type, category, generator, xml = self.ctxt.output.pop()
+        self.assertEqual(Recipe.REPORT, type)
+        self.assertEqual('coverage', category)
+        self.assertEqual(0, len(xml.children))
+
+    def test_missing_coverage_file(self):
+        self.summary.close()
+        pythontools.figleaf(self.ctxt, summary='non-existant-file', include='*.py')
+        self.assertEqual([], self.ctxt.output)
+
+    def test_summary_with_absolute_path(self):
+        filename = '%s/test/module.py' % self.ctxt.basedir
+        pickle.dump({
+            filename: set([1, 4, 5]),
+        }, self.summary)
+        self.summary.close()
+        sourcefile = self.ctxt.resolve(self._create_file('test', 'module.py'))
+        open(sourcefile, 'w').write(
+            "if foo: # line 1\n"
+            "  print 'uncovered' # line 2\n"
+            "else: # line 3 (uninteresting)\n"
+            "  print 'covered' # line 4\n"
+            "print 'covered' # line 6\n"
+        )
+        pythontools.figleaf(self.ctxt, summary=self.summary.name,
+                            include='test/*')
+        type, category, generator, xml = self.ctxt.output.pop()
+        self.assertEqual(Recipe.REPORT, type)
+        self.assertEqual('coverage', category)
+        self.assertEqual(1, len(xml.children))
+        child = xml.children[0]
+        self.assertEqual('coverage', child.name)
+        self.assertEqual('test.module', child.attr['name'])
+        self.assertEqual('test/module.py', child.attr['file'])
+        self.assertEqual(75, child.attr['percentage'])
+        self.assertEqual(4, child.attr['lines'])
+        self.assertEqual('1 0 - 1 1', child.attr['line_hits'])
+
+    def test_summary_with_non_covered_file(self):
+        pickle.dump({}, self.summary)
+        self.summary.close()
+        sourcefile = self.ctxt.resolve(self._create_file('test', 'module.py'))
+        open(sourcefile, 'w').write(
+            "print 'line 1'\n"
+            "print 'line 2'\n"
+            "print 'line 3'\n"
+            "print 'line 4'\n"
+            "print 'line 5'\n"
+        )
+        pythontools.figleaf(self.ctxt, summary=self.summary.name,
+                            include='test/*')
+        type, category, generator, xml = self.ctxt.output.pop()
+        self.assertEqual(Recipe.REPORT, type)
+        self.assertEqual('coverage', category)
+        self.assertEqual(1, len(xml.children))
+        child = xml.children[0]
+        self.assertEqual('coverage', child.name)
+        self.assertEqual('test.module', child.attr['name'])
+        self.assertEqual('test/module.py', child.attr['file'])
+        self.assertEqual(0, child.attr['percentage'])
+        self.assertEqual(5, child.attr['lines'])
+
+    def test_summary_with_non_python_files(self):
+        "Figleaf coverage reports should not include files that do not end in .py"
+        pickle.dump({}, self.summary)
+        self.summary.close()
+        sourcefile = self.ctxt.resolve(self._create_file('test', 'document.txt'))
+        open(sourcefile, 'w').write("\n")
+        pythontools.figleaf(self.ctxt, summary=self.summary.name,
+                            include='test/*')
+        type, category, generator, xml = self.ctxt.output.pop()
+        self.assertEqual(Recipe.REPORT, type)
+        self.assertEqual('coverage', category)
+        self.assertEqual(0, len(xml.children))
+
+
+class FilenameNormalizationTestCase(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)
+        os.makedirs(dirname)
+        fd = file(filename, 'w')
+        fd.close()
+        return filename[len(self.basedir) + 1:]
+
+    def test_absolute_path(self):
+        filename = '%s/test/module.py' % self.ctxt.basedir
+        self._create_file('test', 'module.py')
+        filenames = pythontools._normalize_filenames(
+                            self.ctxt, [filename],
+                            FileSet(self.ctxt.basedir, '**/*.py', None))
+        self.assertEqual(['test/module.py'], list(filenames))
+
+
 class UnittestTestCase(unittest.TestCase):
 
     def setUp(self):
@@ -273,6 +404,8 @@
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(CoverageTestCase, 'test'))
     suite.addTest(unittest.makeSuite(TraceTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(FigleafTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(FilenameNormalizationTestCase, 'test'))
     suite.addTest(unittest.makeSuite(UnittestTestCase, 'test'))
     return suite
 
--- a/bitten/util/testrunner.py
+++ b/bitten/util/testrunner.py
@@ -1,7 +1,8 @@
 # -*- coding: utf-8 -*-
 #
 # Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
-# Copyright (C) 2007 Edgewall Software
+# Copyright (C) 2008 Matt Good <matt@matt-good.net>
+# Copyright (C) 2008 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -131,7 +132,7 @@
                 os.makedirs(os.path.dirname(self.xml_output))
             self.xml_output_file = open(self.xml_output, 'w')
 
-        if self.coverage_method not in ('trace', 'coverage'):
+        if self.coverage_method not in ('trace', 'coverage', 'figleaf'):
             raise DistutilsOptionError('Unknown coverage method %r' %
                                        self.coverage_method)
 
@@ -139,11 +140,22 @@
         if self.coverage_summary:
             if self.coverage_method == 'coverage':
                 self._run_with_coverage()
+            elif self.coverage_method == 'figleaf':
+                self._run_with_figleaf()
             else:
                 self._run_with_trace()
         else:
             self._run_tests()
 
+    def _run_with_figleaf(self):
+        import figleaf
+        figleaf.start()
+        try:
+            self._run_tests()
+        finally:
+            figleaf.stop()
+            figleaf.write_coverage(self.coverage_summary)
+
     def _run_with_coverage(self):
         import coverage
         coverage.use_cache(False)
--- a/setup.py
+++ b/setup.py
@@ -73,6 +73,7 @@
             NS + 'python#coverage = bitten.build.pythontools:coverage',
             NS + 'python#distutils = bitten.build.pythontools:distutils',
             NS + 'python#exec = bitten.build.pythontools:exec_',
+            NS + 'python#figleaf = bitten.build.pythontools:figleaf',
             NS + 'python#pylint = bitten.build.pythontools:pylint',
             NS + 'python#trace = bitten.build.pythontools:trace',
             NS + 'python#unittest = bitten.build.pythontools:unittest',
Copyright (C) 2012-2017 Edgewall Software