# HG changeset patch # User mgood # Date 1205796316 0 # Node ID b87eda443ffc647dd7b76aad7f7a3ab8fe7fbc5e # Parent 0ec997423fce3d15e996c655848e238bd9abd49f add figleaf coverage support diff --git a/bitten/build/pythontools.py b/bitten/build/pythontools.py --- a/bitten/build/pythontools.py +++ b/bitten/build/pythontools.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2005-2007 Christopher Lenz -# Copyright (C) 2007 Edgewall Software +# Copyright (C) 2008 Matt Good +# 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. diff --git a/bitten/build/tests/pythontools.py b/bitten/build/tests/pythontools.py --- 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 -# Copyright (C) 2007 Edgewall Software +# Copyright (C) 2008 Matt Good +# 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 diff --git a/bitten/util/testrunner.py b/bitten/util/testrunner.py --- a/bitten/util/testrunner.py +++ b/bitten/util/testrunner.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2005-2007 Christopher Lenz -# Copyright (C) 2007 Edgewall Software +# Copyright (C) 2008 Matt Good +# 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) diff --git a/setup.py b/setup.py --- 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',