# HG changeset patch # User mgood # Date 1148654908 0 # Node ID 2ffab7963b8dc025d2e9115d1feb34b55d43b964 # Parent ad18a9a702661817154cb85ee10a1f2764abfaf9 add Java recipe command for parsing code coverage from [http://cobertura.sf.net Cobertura] XML reports diff --git a/bitten/build/javatools.py b/bitten/build/javatools.py --- a/bitten/build/javatools.py +++ b/bitten/build/javatools.py @@ -1,6 +1,7 @@ # -*- coding: iso8859-1 -*- # # Copyright (C) 2005 Christopher Lenz +# Copyright (C) 2006 Matthew Good # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -141,3 +142,74 @@ log.warning('Error opening JUnit results file (%s)', e) except xmlio.ParseError, e: log.warning('Error parsing JUnit results file (%s)', e) + +class _LineCounter(object): + def __init__(self): + self.lines = [] + self.covered = 0 + self.num_lines = 0 + + def __getitem__(self, idx): + if idx >= len(self.lines): + return 0 + return self.lines[idx] + + def __setitem__(self, idx, val): + idx = int(idx) - 1 # 1-indexed to 0-indexed + from itertools import repeat + if idx >= len(self.lines): + self.lines.extend(repeat('-', idx - len(self.lines) + 1)) + self.lines[idx] = val + self.num_lines += 1 + if val != '0': + self.covered += 1 + + def line_hits(self): + return ' '.join(self.lines) + line_hits = property(line_hits) + + def percentage(self): + if self.num_lines == 0: + return 0 + return int(round(self.covered * 100. / self.num_lines)) + percentage = property(percentage) + + +def cobertura(ctxt, file_=None): + assert file_, 'Missing required attribute "file"' + + coverage = xmlio.Fragment() + doc = xmlio.parse(open(ctxt.resolve(file_))) + srcdir = [s.gettext().strip() for ss in doc.children('sources') + for s in ss.children('source')][0] + + classes = [cls for pkgs in doc.children('packages') + for pkg in pkgs.children('package') + for clss in pkg.children('classes') + for cls in clss.children('class')] + + counters = {} + class_names = {} + + for cls in classes: + filename = cls.attr['filename'].replace(os.sep, '/') + name = cls.attr['name'] + if not '$' in name: # ignore internal classes + class_names[filename] = name + counter = counters.get(filename) + if counter is None: + counter = counters[filename] = _LineCounter() + lines = [l for ls in cls.children('lines') + for l in ls.children('line')] + for line in lines: + counter[line.attr['number']] = line.attr['hits'] + + for filename, name in class_names.iteritems(): + counter = counters[filename] + module = xmlio.Element('coverage', name=name, + file=posixpath.join(srcdir, filename), + lines=counter.num_lines, + percentage=counter.percentage) + module.append(xmlio.Element('line_hits')[counter.line_hits]) + coverage.append(module) + ctxt.report('coverage', coverage) diff --git a/bitten/build/tests/javatools.py b/bitten/build/tests/javatools.py new file mode 100644 --- /dev/null +++ b/bitten/build/tests/javatools.py @@ -0,0 +1,128 @@ +# -*- coding: iso8859-1 -*- +# +# Copyright (C) 2006 Christopher Lenz +# Copyright (C) 2006 Matthew Good +# 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. + +import os.path +import shutil +import tempfile +import unittest + +from bitten.build import javatools +from bitten.recipe import Context + +class CoberturaTestCase(unittest.TestCase): + xml_template=""" + + + + + src + + + + %s + + + +""" + + 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, **kw): + filename = os.path.join(self.basedir, *path) + dirname = os.path.dirname(filename) + if not os.path.exists(dirname): + os.makedirs(dirname) + fd = file(filename, 'w') + content = kw.get('content') + if content is not None: + fd.write(content) + fd.close() + return filename[len(self.basedir) + 1:] + + def test_basic(self): + filename = self._create_file('coverage.xml', content=self.xml_template % """ + + + + + + + + """) + javatools.cobertura(self.ctxt, file_=filename) + 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('src/test/TestClass.java', elem.attr['file']) + self.assertEqual('test.TestClass', elem.attr['name']) + self.assertEqual(4, elem.attr['lines']) + self.assertEqual(50, elem.attr['percentage']) + + def test_skipped_lines(self): + filename = self._create_file('coverage.xml', content=self.xml_template % """ + + + + + + """) + javatools.cobertura(self.ctxt, file_=filename) + 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('src/test/TestClass.java', elem.attr['file']) + self.assertEqual('test.TestClass', elem.attr['name']) + self.assertEqual(2, elem.attr['lines']) + self.assertEqual(50, elem.attr['percentage']) + + line_hits = elem.children[0] + self.assertEqual('line_hits', line_hits.name) + self.assertEqual('0 - 1', line_hits.children[0]) + + def test_interface(self): + filename = self._create_file('coverage.xml', content=self.xml_template % """ + + + + """) + javatools.cobertura(self.ctxt, file_=filename) + 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('src/test/TestInterface.java', elem.attr['file']) + self.assertEqual('test.TestInterface', elem.attr['name']) + self.assertEqual(0, elem.attr['lines']) + self.assertEqual(0, elem.attr['percentage']) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(CoberturaTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ NS + 'c#make = bitten.build.ctools:make', NS + 'java#ant = bitten.build.javatools:ant', NS + 'java#junit = bitten.build.javatools:junit', + NS + 'java#cobertura = bitten.build.javatools:cobertura', NS + 'python#distutils = bitten.build.pythontools:distutils', NS + 'python#exec = bitten.build.pythontools:exec_', NS + 'python#pylint = bitten.build.pythontools:pylint',