changeset 125:92e29e6b4c16

* The {{{python:trace>}}} recipe command now transmits coverage statistics to the build master. Closes #33. * Added unit tests for {{{<python:trace>}}} and {{{<python:unittest>}}}.
author cmlenz
date Tue, 09 Aug 2005 18:02:21 +0000
parents 540ffa358d80
children c2877512beb8
files bitten/build/pythontools.py bitten/build/tests/__init__.py bitten/build/tests/pythontools.py bitten/tests/__init__.py bitten/tests/recipe.py bitten/tests/store.py
diffstat 6 files changed, 231 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/bitten/build/pythontools.py
+++ b/bitten/build/pythontools.py
@@ -19,6 +19,7 @@
 # Author: Christopher Lenz <cmlenz@gmx.de>
 
 import logging
+import os
 import re
 
 from bitten.build import BuildError
@@ -84,6 +85,49 @@
     assert summary, 'Missing required attribute "summary"'
     assert coverdir, 'Missing required attribute "coverdir"'
 
+    summary_line_re = re.compile(r'^\s*(?P<lines>\d+)\s+(?P<cov>\d+)%\s+'
+                                 r'(?P<module>.*?)\s+\((?P<filename>.*?)\)')
+    coverage_line_re = re.compile(r'\s*(?:(?P<hits>\d+): )?(?P<line>.*)')
+
+    try:
+        summary_file = open(ctxt.resolve(summary), 'r')
+        try:
+            coverage = xmlio.Fragment()
+            for summary_line in summary_file:
+                match = summary_line_re.search(summary_line)
+                if match:
+                    filename = match.group(4)
+                    modname = match.group(3)
+                    cov = int(match.group(2))
+                    if filename.startswith(ctxt.basedir):
+                        module = xmlio.Element('coverage', file=filename,
+                                               module=modname, percentage=cov)
+                        coverage_path = ctxt.resolve(coverdir,
+                                                     modname + '.cover')
+                        if not os.path.exists(coverage_path):
+                            log.warning('No coverage file for module %s at %s',
+                                        modname, coverage_path)
+                            continue
+                        coverage_file = open(coverage_path, 'r')
+                        try:
+                            for num, coverage_line in enumerate(coverage_file):
+                                match = coverage_line_re.search(coverage_line)
+                                if match:
+                                    hits = match.group(1)
+                                    if hits:
+                                        line = xmlio.Element('line', line=num,
+                                                             hits=int(hits))
+                                        module.append(line)
+                        finally:
+                            coverage_file.close()
+                        coverage.append(module)
+            ctxt.report(coverage)
+        finally:
+            summary_file.close()
+    except IOError, e:
+        raise BuildError, 'Error opening unittest results file (%s)' % e
+
+
 def unittest(ctxt, file=None):
     """Extract data from a unittest results file in XML format."""
     assert file, 'Missing required attribute "file"'
@@ -93,6 +137,9 @@
         try:
             results = xmlio.Fragment()
             for child in xmlio.parse(fd).children():
+                filename = child.attr.get('file')
+                if filename and filename.startswith(ctxt.basedir):
+                    child.attr['file'] = filename[len(ctxt.basedir) + 1:]
                 results.append(child)
             ctxt.report(results)
         finally:
new file mode 100644
--- /dev/null
+++ b/bitten/build/tests/__init__.py
@@ -0,0 +1,32 @@
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+#
+# Bitten is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the
+# License, or (at your option) any later version.
+#
+# Trac is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+import doctest
+import unittest
+
+from bitten.build.tests import pythontools
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(pythontools.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/bitten/build/tests/pythontools.py
@@ -0,0 +1,143 @@
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+#
+# Bitten is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the
+# License, or (at your option) any later version.
+#
+# Trac is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+import os
+import os.path
+import shutil
+import tempfile
+import unittest
+
+from bitten.build import pythontools, BuildError
+from bitten.recipe import Context, Recipe
+
+
+class TraceTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.temp_dir = tempfile.gettempdir()
+        self.ctxt = Context(self.temp_dir)
+        self.summary = open(os.path.join(self.temp_dir, 'test-coverage.txt'),
+                            'w')
+        self.coverdir = os.path.join(self.temp_dir, 'coverage')
+        os.mkdir(self.coverdir)
+
+    def tearDown(self):
+        shutil.rmtree(self.coverdir)
+        os.unlink(self.summary.name)
+
+    def test_missing_param_summary(self):
+        self.assertRaises(AssertionError, pythontools.trace, self.ctxt,
+                          coverdir='coverage')
+
+    def test_missing_param_coverdir(self):
+        self.assertRaises(AssertionError, pythontools.trace, self.ctxt,
+                          summary='test-coverage.txt')
+
+    def test_empty_summary(self):
+        self.summary.write('line  cov%  module  (path)')
+        self.summary.close()
+        pythontools.trace(self.ctxt, summary=self.summary.name,
+                          coverdir=self.coverdir)
+        type, function, xml = self.ctxt.output.pop()
+        self.assertEqual(Recipe.REPORT, type)
+        self.assertEqual(0, len(xml.children))
+
+
+
+class UnittestTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.temp_dir = tempfile.gettempdir()
+        self.ctxt = Context(self.temp_dir)
+        self.results_xml = open(os.path.join(self.temp_dir, 'test-results.xml'),
+                                'w')
+
+    def tearDown(self):
+        os.unlink(os.path.join(self.temp_dir, 'test-results.xml'))
+
+    def test_missing_file_param(self):
+        self.assertRaises(AssertionError, pythontools.unittest, self.ctxt)
+
+    def test_invalid_file_param(self):
+        self.assertRaises(BuildError,
+                          pythontools.unittest, self.ctxt, file='foobar')
+
+    def test_empty_results(self):
+        self.results_xml.write('<?xml version="1.0"?>'
+                              '<unittest-results>'
+                              '</unittest-results>')
+        self.results_xml.close()
+        pythontools.unittest(self.ctxt, self.results_xml.name)
+        type, function, xml = self.ctxt.output.pop()
+        self.assertEqual(Recipe.REPORT, type)
+        self.assertEqual(0, len(xml.children))
+
+    def test_successful_test(self):
+        self.results_xml.write('<?xml version="1.0"?>'
+                              '<unittest-results>'
+                              '<test duration="0.12" status="success"'
+                              '      file="bar_test.py"'
+                              '      name="test_foo (pkg.BarTestCase)"/>'
+                              '</unittest-results>')
+        self.results_xml.close()
+        pythontools.unittest(self.ctxt, self.results_xml.name)
+        type, function, xml = self.ctxt.output.pop()
+        self.assertEqual(Recipe.REPORT, type)
+        self.assertEqual(1, len(xml.children))
+        test_elem = xml.children[0]
+        self.assertEqual('test', test_elem.name)
+        self.assertEqual('0.12', test_elem.attr['duration'])
+        self.assertEqual('success', test_elem.attr['status'])
+        self.assertEqual('bar_test.py', test_elem.attr['file'])
+        self.assertEqual('test_foo (pkg.BarTestCase)', test_elem.attr['name'])
+
+    def test_file_path_normalization(self):
+        self.results_xml.write('<?xml version="1.0"?>'
+                              '<unittest-results>'
+                              '<test duration="0.12" status="success"'
+                              '      file="%s"'
+                              '      name="test_foo (pkg.BarTestCase)"/>'
+                              '</unittest-results>'
+                              % os.path.join(self.ctxt.basedir, 'bar_test.py'))
+        self.results_xml.close()
+        pythontools.unittest(self.ctxt, self.results_xml.name)
+        type, function, xml = self.ctxt.output.pop()
+        self.assertEqual('bar_test.py', xml.children[0].attr['file'])
+
+    def test_missing_file_attribute(self):
+        self.results_xml.write('<?xml version="1.0"?>'
+                              '<unittest-results>'
+                              '<test duration="0.12" status="success"'
+                              '      name="test_foo (pkg.BarTestCase)"/>'
+                              '</unittest-results>')
+        self.results_xml.close()
+        pythontools.unittest(self.ctxt, self.results_xml.name)
+        type, function, xml = self.ctxt.output.pop()
+        self.assertEqual(None, xml.children[0].attr.get('file'))
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(TraceTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(UnittestTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
--- a/bitten/tests/__init__.py
+++ b/bitten/tests/__init__.py
@@ -21,6 +21,7 @@
 import unittest
 
 from bitten.tests import model, recipe, store
+from bitten.build import tests as build
 from bitten.util import tests as util
 from bitten.trac_ext import tests as trac_ext
 
@@ -29,6 +30,7 @@
     suite.addTest(model.suite())
     suite.addTest(recipe.suite())
     suite.addTest(store.suite())
+    suite.addTest(build.suite())
     suite.addTest(trac_ext.suite())
     suite.addTest(util.suite())
     return suite
--- a/bitten/tests/recipe.py
+++ b/bitten/tests/recipe.py
@@ -44,5 +44,9 @@
         recipe = Recipe(basedir=self.temp_dir)
         self.assertEqual('test', recipe.description)
 
+
 def suite():
     return unittest.makeSuite(RecipeTestCase, 'test')
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
--- a/bitten/tests/store.py
+++ b/bitten/tests/store.py
@@ -70,3 +70,6 @@
     except ImportError:
         print>>sys.stderr, 'Skipping unit tests for BDB XML backend'
     return unittest.TestSuite()
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
Copyright (C) 2012-2017 Edgewall Software