changeset 409:5786700df0c7

Moved/restructured the modules implementing report chart generators and report summarizers.
author cmlenz
date Tue, 07 Aug 2007 09:17:30 +0000
parents 933105ab516b
children 7930cdd83d13
files bitten/report/__init__.py bitten/report/coverage.py bitten/report/testing.py bitten/report/tests/__init__.py bitten/report/tests/coverage.py bitten/report/tests/testing.py bitten/tests/__init__.py bitten/trac_ext/charts.py bitten/trac_ext/summarizers.py bitten/trac_ext/tests/__init__.py bitten/trac_ext/tests/charts.py bitten/trac_ext/web_ui.py setup.py
diffstat 13 files changed, 518 insertions(+), 460 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/bitten/report/__init__.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007 Edgewall Software
+# 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.edgewall.org/wiki/License.
new file mode 100644
--- /dev/null
+++ b/bitten/report/coverage.py
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2007 Edgewall Software
+# 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.edgewall.org/wiki/License.
+
+from trac.core import *
+from trac.web.chrome import Chrome
+from trac.web.clearsilver import HDFWrapper
+from bitten.trac_ext.api import IReportChartGenerator, IReportSummarizer
+
+
+class TestCoverageChartGenerator(Component):
+    implements(IReportChartGenerator)
+
+    # IReportChartGenerator methods
+
+    def get_supported_categories(self):
+        return ['coverage']
+
+    def generate_chart_data(self, req, config, category):
+        assert category == 'coverage'
+
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("""
+SELECT build.rev, SUM(%s) AS loc, SUM(%s * %s / 100) AS cov
+FROM bitten_build AS build
+ LEFT OUTER JOIN bitten_report AS report ON (report.build=build.id)
+ LEFT OUTER JOIN bitten_report_item AS item_lines
+  ON (item_lines.report=report.id AND item_lines.name='lines')
+ LEFT OUTER JOIN bitten_report_item AS item_percentage
+  ON (item_percentage.report=report.id AND item_percentage.name='percentage' AND
+      item_percentage.item=item_lines.item)
+WHERE build.config=%%s AND report.category='coverage'
+GROUP BY build.rev_time, build.rev, build.platform
+ORDER BY build.rev_time""" % (db.cast('item_lines.value', 'int'),
+                              db.cast('item_lines.value', 'int'),
+                              db.cast('item_percentage.value', 'int')),
+                              (config.name,))
+
+        prev_rev = None
+        coverage = []
+        for rev, loc, cov in cursor:
+            if rev != prev_rev:
+                coverage.append([rev, 0, 0])
+            if loc > coverage[-1][1]:
+                coverage[-1][1] = int(loc)
+            if cov > coverage[-1][2]:
+                coverage[-1][2] = int(cov)
+            prev_rev = rev
+
+        req.hdf['chart.title'] = 'Test Coverage'
+        req.hdf['chart.data'] = [
+            [''] + ['[%s]' % item[0] for item in coverage],
+            ['Lines of code'] + [item[1] for item in coverage],
+            ['Coverage'] + [int(item[2]) for item in coverage]
+        ]
+
+        return 'bitten_chart_coverage.cs'
+
+
+class TestCoverageSummarizer(Component):
+    implements(IReportSummarizer)
+
+    # IReportSummarizer methods
+
+    def get_supported_categories(self):
+        return ['coverage']
+
+    def render_summary(self, req, config, build, step, category):
+        assert category == 'coverage'
+
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("""
+SELECT item_name.value AS unit, item_file.value AS file,
+       max(item_lines.value) AS loc, max(item_percentage.value) AS cov
+FROM bitten_report AS report
+ LEFT OUTER JOIN bitten_report_item AS item_name
+  ON (item_name.report=report.id AND item_name.name='name')
+ LEFT OUTER JOIN bitten_report_item AS item_file
+  ON (item_file.report=report.id AND item_file.item=item_name.item AND
+      item_file.name='file')
+ LEFT OUTER JOIN bitten_report_item AS item_lines
+  ON (item_lines.report=report.id AND item_lines.item=item_name.item AND
+      item_lines.name='lines')
+ LEFT OUTER JOIN bitten_report_item AS item_percentage
+  ON (item_percentage.report=report.id AND
+      item_percentage.item=item_name.item AND
+      item_percentage.name='percentage')
+WHERE category='coverage' AND build=%s AND step=%s
+GROUP BY file, item_name.value
+ORDER BY item_name.value""", (build.id, step.name))
+
+        data = []
+        total_loc, total_cov = 0, 0
+        for unit, file, loc, cov in cursor:
+            try:
+                loc, cov = int(loc), float(cov)
+            except TypeError:
+                continue # no rows
+            if loc:
+                d = {'name': unit, 'loc': loc, 'cov': int(cov)}
+                if file:
+                    d['href'] = req.href.browser(config.path, file)
+                data.append(d)
+                total_loc += loc
+                total_cov += loc * cov
+
+        coverage = 0
+        if total_loc != 0:
+            coverage = total_cov // total_loc
+
+        hdf = HDFWrapper(loadpaths=Chrome(self.env).get_all_templates_dirs())
+        hdf['data'] = data
+        hdf['totals'] = {'loc': total_loc, 'cov': int(coverage)}
+        return hdf.render('bitten_summary_coverage.cs')
new file mode 100644
--- /dev/null
+++ b/bitten/report/testing.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2007 Edgewall Software
+# 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.edgewall.org/wiki/License.
+
+from trac.core import *
+from trac.web.chrome import Chrome
+from trac.web.clearsilver import HDFWrapper
+from bitten.trac_ext.api import IReportChartGenerator, IReportSummarizer
+
+
+class TestResultsChartGenerator(Component):
+    implements(IReportChartGenerator)
+
+    # IReportChartGenerator methods
+
+    def get_supported_categories(self):
+        return ['test']
+
+    def generate_chart_data(self, req, config, category):
+        assert category == 'test'
+
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("""
+SELECT build.rev, build.platform, item_status.value AS status, COUNT(*) AS num
+FROM bitten_build AS build
+ LEFT OUTER JOIN bitten_report AS report ON (report.build=build.id)
+ LEFT OUTER JOIN bitten_report_item AS item_status
+  ON (item_status.report=report.id AND item_status.name='status')
+WHERE build.config=%s AND report.category='test'
+GROUP BY build.rev_time, build.rev, build.platform, item_status.value
+ORDER BY build.rev_time, build.platform""", (config.name,))
+
+        prev_rev = None
+        prev_platform, platform_total = None, 0
+        tests = []
+        for rev, platform, status, num in cursor:
+            if rev != prev_rev:
+                tests.append([rev, 0, 0])
+                prev_rev = rev
+                platform_total = 0
+            if platform != prev_platform:
+                prev_platform = platform
+                platform_total = 0
+
+            platform_total += num
+            tests[-1][1] = max(platform_total, tests[-1][1])
+            if status != 'success':
+                tests[-1][2] = max(num, tests[-1][2])
+
+        req.hdf['chart.title'] = 'Unit Tests'
+        req.hdf['chart.data'] = [
+            [''] + ['[%s]' % item[0] for item in tests],
+            ['Total'] + [item[1] for item in tests],
+            ['Failures'] + [item[2] for item in tests]
+        ]
+
+        return 'bitten_chart_tests.cs'
+
+
+class TestResultsSummarizer(Component):
+    implements(IReportSummarizer)
+
+    # IReportSummarizer methods
+
+    def get_supported_categories(self):
+        return ['test']
+
+    def render_summary(self, req, config, build, step, category):
+        assert category == 'test'
+
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("""
+SELECT item_fixture.value AS fixture, item_file.value AS file,
+       COUNT(item_success.value) AS num_success,
+       COUNT(item_failure.value) AS num_failure,
+       COUNT(item_error.value) AS num_error
+FROM bitten_report AS report
+ LEFT OUTER JOIN bitten_report_item AS item_fixture
+  ON (item_fixture.report=report.id AND item_fixture.name='fixture')
+ LEFT OUTER JOIN bitten_report_item AS item_file
+  ON (item_file.report=report.id AND item_file.item=item_fixture.item AND
+      item_file.name='file')
+ LEFT OUTER JOIN bitten_report_item AS item_success
+  ON (item_success.report=report.id AND item_success.item=item_fixture.item AND
+      item_success.name='status' AND item_success.value='success')
+ LEFT OUTER JOIN bitten_report_item AS item_failure
+  ON (item_failure.report=report.id AND item_failure.item=item_fixture.item AND
+      item_failure.name='status' AND item_failure.value='failure')
+ LEFT OUTER JOIN bitten_report_item AS item_error
+  ON (item_error.report=report.id AND item_error.item=item_fixture.item AND
+      item_error.name='status' AND item_error.value='error')
+WHERE category='test' AND build=%s AND step=%s
+GROUP BY file, fixture
+ORDER BY fixture""", (build.id, step.name))
+
+        data = []
+        total_success, total_failure, total_error = 0, 0, 0
+        for fixture, file, num_success, num_failure, num_error in cursor:
+            data.append({'name': fixture, 'num_success': num_success,
+                         'num_error': num_error, 'num_failure': num_failure})
+            total_success += num_success
+            total_failure += num_failure
+            total_error += num_error
+            if file:
+                data[-1]['href'] = req.href.browser(config.path, file)
+
+        hdf = HDFWrapper(loadpaths=Chrome(self.env).get_all_templates_dirs())
+        hdf['data'] = data
+        hdf['totals'] = {'success': total_success, 'failure': total_failure,
+                         'error': total_error}
+        return hdf.render('bitten_summary_tests.cs')
new file mode 100644
--- /dev/null
+++ b/bitten/report/tests/__init__.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2007 Edgewall Software
+# 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.edgewall.org/wiki/License.
+
+import unittest
+
+from bitten.report.tests import coverage, testing
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(coverage.suite())
+    suite.addTest(testing.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/bitten/report/tests/coverage.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2007 Edgewall Software
+# 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.edgewall.org/wiki/License.
+
+import unittest
+
+from trac.test import EnvironmentStub, Mock
+from trac.web.clearsilver import HDFWrapper
+from bitten.model import *
+from bitten.report.coverage import TestCoverageChartGenerator
+from bitten.trac_ext.compat import schema_to_sql
+
+
+class TestCoverageChartGeneratorTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+        self.env.path = ''
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        for table in schema:
+            for stmt in schema_to_sql(self.env, db, table):
+                cursor.execute(stmt)
+
+    def test_supported_categories(self):
+        generator = TestCoverageChartGenerator(self.env)
+        self.assertEqual(['coverage'], generator.get_supported_categories())
+
+    def test_no_reports(self):
+        req = Mock(hdf=HDFWrapper())
+        config = Mock(name='trunk')
+        generator = TestCoverageChartGenerator(self.env)
+        template = generator.generate_chart_data(req, config, 'coverage')
+        self.assertEqual('bitten_chart_coverage.cs', template)
+        self.assertEqual('Test Coverage', req.hdf['chart.title'])
+        self.assertEqual('', req.hdf['chart.data.0.0'])
+        self.assertEqual('Lines of code', req.hdf['chart.data.1.0'])
+        self.assertEqual('Coverage', req.hdf['chart.data.2.0'])
+
+    def test_single_platform(self):
+        config = Mock(name='trunk')
+        build = Build(self.env, config='trunk', platform=1, rev=123,
+                      rev_time=42)
+        build.insert()
+        report = Report(self.env, build=build.id, step='foo',
+                        category='coverage')
+        report.items += [{'lines': '12', 'percentage': '25'}]
+        report.insert()
+
+        req = Mock(hdf=HDFWrapper())
+        generator = TestCoverageChartGenerator(self.env)
+        template = generator.generate_chart_data(req, config, 'coverage')
+        self.assertEqual('bitten_chart_coverage.cs', template)
+        self.assertEqual('Test Coverage', req.hdf['chart.title'])
+        self.assertEqual('', req.hdf['chart.data.0.0'])
+        self.assertEqual('[123]', req.hdf['chart.data.0.1'])
+        self.assertEqual('Lines of code', req.hdf['chart.data.1.0'])
+        self.assertEqual('12', req.hdf['chart.data.1.1'])
+        self.assertEqual('Coverage', req.hdf['chart.data.2.0'])
+        self.assertEqual('3', req.hdf['chart.data.2.1'])
+
+    def test_multi_platform(self):
+        config = Mock(name='trunk')
+        build = Build(self.env, config='trunk', platform=1, rev=123,
+                      rev_time=42)
+        build.insert()
+        report = Report(self.env, build=build.id, step='foo',
+                        category='coverage')
+        report.items += [{'lines': '12', 'percentage': '25'}]
+        report.insert()
+        build = Build(self.env, config='trunk', platform=2, rev=123,
+                      rev_time=42)
+        build.insert()
+        report = Report(self.env, build=build.id, step='foo',
+                        category='coverage')
+        report.items += [{'lines': '12', 'percentage': '50'}]
+        report.insert()
+
+        req = Mock(hdf=HDFWrapper())
+        generator = TestCoverageChartGenerator(self.env)
+        template = generator.generate_chart_data(req, config, 'coverage')
+        self.assertEqual('bitten_chart_coverage.cs', template)
+        self.assertEqual('Test Coverage', req.hdf['chart.title'])
+        self.assertEqual('', req.hdf['chart.data.0.0'])
+        self.assertEqual('[123]', req.hdf['chart.data.0.1'])
+        self.assertEqual('Lines of code', req.hdf['chart.data.1.0'])
+        self.assertEqual('12', req.hdf['chart.data.1.1'])
+        self.assertEqual('Coverage', req.hdf['chart.data.2.0'])
+        self.assertEqual('6', req.hdf['chart.data.2.1'])
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(TestCoverageChartGeneratorTestCase))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/bitten/report/tests/testing.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2007 Edgewall Software
+# 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.edgewall.org/wiki/License.
+
+import unittest
+
+from trac.test import EnvironmentStub, Mock
+from trac.web.clearsilver import HDFWrapper
+from bitten.model import *
+from bitten.report.testing import TestResultsChartGenerator
+from bitten.trac_ext.compat import schema_to_sql
+
+
+class TestResultsChartGeneratorTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+        self.env.path = ''
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        for table in schema:
+            for stmt in schema_to_sql(self.env, db, table):
+                cursor.execute(stmt)
+
+    def test_supported_categories(self):
+        generator = TestResultsChartGenerator(self.env)
+        self.assertEqual(['test'], generator.get_supported_categories())
+
+    def test_no_reports(self):
+        req = Mock(hdf=HDFWrapper())
+        config = Mock(name='trunk')
+        generator = TestResultsChartGenerator(self.env)
+        template = generator.generate_chart_data(req, config, 'test')
+        self.assertEqual('bitten_chart_tests.cs', template)
+        self.assertEqual('Unit Tests', req.hdf['chart.title'])
+        self.assertEqual('', req.hdf['chart.data.0.0'])
+        self.assertEqual('Total', req.hdf['chart.data.1.0'])
+        self.assertEqual('Failures', req.hdf['chart.data.2.0'])
+
+    def test_single_platform(self):
+        config = Mock(name='trunk')
+        build = Build(self.env, config='trunk', platform=1, rev=123,
+                      rev_time=42)
+        build.insert()
+        report = Report(self.env, build=build.id, step='foo', category='test')
+        report.items += [{'status': 'success'}, {'status': 'failure'},
+                         {'status': 'success'}]
+        report.insert()
+
+        req = Mock(hdf=HDFWrapper())
+        generator = TestResultsChartGenerator(self.env)
+        template = generator.generate_chart_data(req, config, 'test')
+        self.assertEqual('bitten_chart_tests.cs', template)
+        self.assertEqual('Unit Tests', req.hdf['chart.title'])
+        self.assertEqual('', req.hdf['chart.data.0.0'])
+        self.assertEqual('[123]', req.hdf['chart.data.0.1'])
+        self.assertEqual('Total', req.hdf['chart.data.1.0'])
+        self.assertEqual('3', req.hdf['chart.data.1.1'])
+        self.assertEqual('Failures', req.hdf['chart.data.2.0'])
+        self.assertEqual('1', req.hdf['chart.data.2.1'])
+
+    def test_multi_platform(self):
+        config = Mock(name='trunk')
+
+        build = Build(self.env, config='trunk', platform=1, rev=123,
+                      rev_time=42)
+        build.insert()
+        report = Report(self.env, build=build.id, step='foo', category='test')
+        report.items += [{'status': 'success'}, {'status': 'failure'},
+                         {'status': 'success'}]
+        report.insert()
+
+        build = Build(self.env, config='trunk', platform=2, rev=123,
+                      rev_time=42)
+        build.insert()
+        report = Report(self.env, build=build.id, step='foo', category='test')
+        report.items += [{'status': 'success'}, {'status': 'failure'},
+                         {'status': 'failure'}]
+        report.insert()
+
+        req = Mock(hdf=HDFWrapper())
+        generator = TestResultsChartGenerator(self.env)
+        template = generator.generate_chart_data(req, config, 'test')
+        self.assertEqual('bitten_chart_tests.cs', template)
+        self.assertEqual('Unit Tests', req.hdf['chart.title'])
+        self.assertEqual('', req.hdf['chart.data.0.0'])
+        self.assertEqual('[123]', req.hdf['chart.data.0.1'])
+        self.assertEqual('Total', req.hdf['chart.data.1.0'])
+        self.assertEqual('3', req.hdf['chart.data.1.1'])
+        self.assertEqual('Failures', req.hdf['chart.data.2.0'])
+        self.assertEqual('2', req.hdf['chart.data.2.1'])
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(TestResultsChartGeneratorTestCase))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
--- a/bitten/tests/__init__.py
+++ b/bitten/tests/__init__.py
@@ -12,8 +12,9 @@
 
 from bitten.tests import master, model, recipe, queue, slave
 from bitten.build import tests as build
+from bitten.report import tests as report
+from bitten.trac_ext import tests as trac_ext
 from bitten.util import tests as util
-from bitten.trac_ext import tests as trac_ext
 
 def suite():
     suite = unittest.TestSuite()
@@ -23,6 +24,7 @@
     suite.addTest(queue.suite())
     suite.addTest(slave.suite())
     suite.addTest(build.suite())
+    suite.addTest(report.suite())
     suite.addTest(trac_ext.suite())
     suite.addTest(util.suite())
     return suite
deleted file mode 100644
--- a/bitten/trac_ext/charts.py
+++ /dev/null
@@ -1,145 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
-# Copyright (C) 2007 Edgewall Software
-# 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.edgewall.org/wiki/License.
-
-import re
-
-from trac.core import *
-from trac.web import IRequestHandler
-from bitten.model import BuildConfig
-from bitten.trac_ext.api import IReportChartGenerator
-
-
-class ReportChartController(Component):
-    implements(IRequestHandler)
-
-    generators = ExtensionPoint(IReportChartGenerator)
-
-    # IRequestHandler methods
-
-    def match_request(self, req):
-        match = re.match(r'/build/([\w.-]+)/chart/(\w+)', req.path_info)
-        if match:
-            req.args['config'] = match.group(1)
-            req.args['category'] = match.group(2)
-            return True
-
-    def process_request(self, req):
-        category = req.args.get('category')
-        config = BuildConfig.fetch(self.env, name=req.args.get('config'))
-
-        for generator in self.generators:
-            if category in generator.get_supported_categories():
-                template = generator.generate_chart_data(req, config,
-                                                         category)
-                break
-        else:
-            raise TracError('Unknown report category "%s"' % category)
-
-        return template, 'text/xml'
-
-
-class TestResultsChartGenerator(Component):
-    implements(IReportChartGenerator)
-
-    # IReportChartGenerator methods
-
-    def get_supported_categories(self):
-        return ['test']
-
-    def generate_chart_data(self, req, config, category):
-        assert category == 'test'
-
-        db = self.env.get_db_cnx()
-        cursor = db.cursor()
-        cursor.execute("""
-SELECT build.rev, build.platform, item_status.value AS status, COUNT(*) AS num
-FROM bitten_build AS build
- LEFT OUTER JOIN bitten_report AS report ON (report.build=build.id)
- LEFT OUTER JOIN bitten_report_item AS item_status
-  ON (item_status.report=report.id AND item_status.name='status')
-WHERE build.config=%s AND report.category='test'
-GROUP BY build.rev_time, build.rev, build.platform, item_status.value
-ORDER BY build.rev_time, build.platform""", (config.name,))
-
-        prev_rev = None
-        prev_platform, platform_total = None, 0
-        tests = []
-        for rev, platform, status, num in cursor:
-            if rev != prev_rev:
-                tests.append([rev, 0, 0])
-                prev_rev = rev
-                platform_total = 0
-            if platform != prev_platform:
-                prev_platform = platform
-                platform_total = 0
-
-            platform_total += num
-            tests[-1][1] = max(platform_total, tests[-1][1])
-            if status != 'success':
-                tests[-1][2] = max(num, tests[-1][2])
-
-        req.hdf['chart.title'] = 'Unit Tests'
-        req.hdf['chart.data'] = [
-            [''] + ['[%s]' % item[0] for item in tests],
-            ['Total'] + [item[1] for item in tests],
-            ['Failures'] + [item[2] for item in tests]
-        ]
-
-        return 'bitten_chart_tests.cs'
-
-
-class TestCoverageChartGenerator(Component):
-    implements(IReportChartGenerator)
-
-    # IReportChartGenerator methods
-
-    def get_supported_categories(self):
-        return ['coverage']
-
-    def generate_chart_data(self, req, config, category):
-        assert category == 'coverage'
-
-        db = self.env.get_db_cnx()
-        cursor = db.cursor()
-        cursor.execute("""
-SELECT build.rev, SUM(%s) AS loc, SUM(%s * %s / 100) AS cov
-FROM bitten_build AS build
- LEFT OUTER JOIN bitten_report AS report ON (report.build=build.id)
- LEFT OUTER JOIN bitten_report_item AS item_lines
-  ON (item_lines.report=report.id AND item_lines.name='lines')
- LEFT OUTER JOIN bitten_report_item AS item_percentage
-  ON (item_percentage.report=report.id AND item_percentage.name='percentage' AND
-      item_percentage.item=item_lines.item)
-WHERE build.config=%%s AND report.category='coverage'
-GROUP BY build.rev_time, build.rev, build.platform
-ORDER BY build.rev_time""" % (db.cast('item_lines.value', 'int'),
-                              db.cast('item_lines.value', 'int'),
-                              db.cast('item_percentage.value', 'int')),
-                              (config.name,))
-
-        prev_rev = None
-        coverage = []
-        for rev, loc, cov in cursor:
-            if rev != prev_rev:
-                coverage.append([rev, 0, 0])
-            if loc > coverage[-1][1]:
-                coverage[-1][1] = int(loc)
-            if cov > coverage[-1][2]:
-                coverage[-1][2] = int(cov)
-            prev_rev = rev
-
-        req.hdf['chart.title'] = 'Test Coverage'
-        req.hdf['chart.data'] = [
-            [''] + ['[%s]' % item[0] for item in coverage],
-            ['Lines of code'] + [item[1] for item in coverage],
-            ['Coverage'] + [int(item[2]) for item in coverage]
-        ]
-
-        return 'bitten_chart_coverage.cs'
deleted file mode 100644
--- a/bitten/trac_ext/summarizers.py
+++ /dev/null
@@ -1,123 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
-# Copyright (C) 2007 Edgewall Software
-# 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.edgewall.org/wiki/License.
-
-from trac.core import *
-from trac.web.chrome import Chrome
-from trac.web.clearsilver import HDFWrapper
-from bitten.trac_ext.api import IReportSummarizer
-
-
-class TestResultsSummarizer(Component):
-    implements(IReportSummarizer)
-
-    def get_supported_categories(self):
-        return ['test']
-
-    def render_summary(self, req, config, build, step, category):
-        assert category == 'test'
-
-        db = self.env.get_db_cnx()
-        cursor = db.cursor()
-        cursor.execute("""
-SELECT item_fixture.value AS fixture, item_file.value AS file,
-       COUNT(item_success.value) AS num_success,
-       COUNT(item_failure.value) AS num_failure,
-       COUNT(item_error.value) AS num_error
-FROM bitten_report AS report
- LEFT OUTER JOIN bitten_report_item AS item_fixture
-  ON (item_fixture.report=report.id AND item_fixture.name='fixture')
- LEFT OUTER JOIN bitten_report_item AS item_file
-  ON (item_file.report=report.id AND item_file.item=item_fixture.item AND
-      item_file.name='file')
- LEFT OUTER JOIN bitten_report_item AS item_success
-  ON (item_success.report=report.id AND item_success.item=item_fixture.item AND
-      item_success.name='status' AND item_success.value='success')
- LEFT OUTER JOIN bitten_report_item AS item_failure
-  ON (item_failure.report=report.id AND item_failure.item=item_fixture.item AND
-      item_failure.name='status' AND item_failure.value='failure')
- LEFT OUTER JOIN bitten_report_item AS item_error
-  ON (item_error.report=report.id AND item_error.item=item_fixture.item AND
-      item_error.name='status' AND item_error.value='error')
-WHERE category='test' AND build=%s AND step=%s
-GROUP BY file, fixture
-ORDER BY fixture""", (build.id, step.name))
-
-        data = []
-        total_success, total_failure, total_error = 0, 0, 0
-        for fixture, file, num_success, num_failure, num_error in cursor:
-            data.append({'name': fixture, 'num_success': num_success,
-                         'num_error': num_error, 'num_failure': num_failure})
-            total_success += num_success
-            total_failure += num_failure
-            total_error += num_error
-            if file:
-                data[-1]['href'] = req.href.browser(config.path, file)
-
-        hdf = HDFWrapper(loadpaths=Chrome(self.env).get_all_templates_dirs())
-        hdf['data'] = data
-        hdf['totals'] = {'success': total_success, 'failure': total_failure,
-                         'error': total_error}
-        return hdf.render('bitten_summary_tests.cs')
-
-
-class TestCoverageSummarizer(Component):
-    implements(IReportSummarizer)
-
-    def get_supported_categories(self):
-        return ['coverage']
-
-    def render_summary(self, req, config, build, step, category):
-        assert category == 'coverage'
-
-        db = self.env.get_db_cnx()
-        cursor = db.cursor()
-        cursor.execute("""
-SELECT item_name.value AS unit, item_file.value AS file,
-       max(item_lines.value) AS loc, max(item_percentage.value) AS cov
-FROM bitten_report AS report
- LEFT OUTER JOIN bitten_report_item AS item_name
-  ON (item_name.report=report.id AND item_name.name='name')
- LEFT OUTER JOIN bitten_report_item AS item_file
-  ON (item_file.report=report.id AND item_file.item=item_name.item AND
-      item_file.name='file')
- LEFT OUTER JOIN bitten_report_item AS item_lines
-  ON (item_lines.report=report.id AND item_lines.item=item_name.item AND
-      item_lines.name='lines')
- LEFT OUTER JOIN bitten_report_item AS item_percentage
-  ON (item_percentage.report=report.id AND
-      item_percentage.item=item_name.item AND
-      item_percentage.name='percentage')
-WHERE category='coverage' AND build=%s AND step=%s
-GROUP BY file, item_name.value
-ORDER BY item_name.value""", (build.id, step.name))
-
-        data = []
-        total_loc, total_cov = 0, 0
-        for unit, file, loc, cov in cursor:
-            try:
-                loc, cov = int(loc), float(cov)
-            except TypeError:
-                continue # no rows
-            if loc:
-                d = {'name': unit, 'loc': loc, 'cov': int(cov)}
-                if file:
-                    d['href'] = req.href.browser(config.path, file)
-                data.append(d)
-                total_loc += loc
-                total_cov += loc * cov
-
-        coverage = 0
-        if total_loc != 0:
-            coverage = total_cov // total_loc
-
-        hdf = HDFWrapper(loadpaths=Chrome(self.env).get_all_templates_dirs())
-        hdf['data'] = data
-        hdf['totals'] = {'loc': total_loc, 'cov': int(coverage)}
-        return hdf.render('bitten_summary_coverage.cs')
--- a/bitten/trac_ext/tests/__init__.py
+++ b/bitten/trac_ext/tests/__init__.py
@@ -10,11 +10,10 @@
 
 import unittest
 
-from bitten.trac_ext.tests import charts, web_ui
+from bitten.trac_ext.tests import web_ui
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(charts.suite())
     suite.addTest(web_ui.suite())
     return suite
 
deleted file mode 100644
--- a/bitten/trac_ext/tests/charts.py
+++ /dev/null
@@ -1,185 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
-# Copyright (C) 2007 Edgewall Software
-# 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.edgewall.org/wiki/License.
-
-import unittest
-
-from trac.test import EnvironmentStub, Mock
-from trac.web.clearsilver import HDFWrapper
-from bitten.model import *
-from bitten.trac_ext.charts import *
-from bitten.trac_ext.compat import schema_to_sql
-
-
-class TestResultsChartGeneratorTestCase(unittest.TestCase):
-
-    def setUp(self):
-        self.env = EnvironmentStub()
-        self.env.path = ''
-        db = self.env.get_db_cnx()
-        cursor = db.cursor()
-        for table in schema:
-            for stmt in schema_to_sql(self.env, db, table):
-                cursor.execute(stmt)
-
-    def test_supported_categories(self):
-        generator = TestResultsChartGenerator(self.env)
-        self.assertEqual(['test'], generator.get_supported_categories())
-
-    def test_no_reports(self):
-        req = Mock(hdf=HDFWrapper())
-        config = Mock(name='trunk')
-        generator = TestResultsChartGenerator(self.env)
-        template = generator.generate_chart_data(req, config, 'test')
-        self.assertEqual('bitten_chart_tests.cs', template)
-        self.assertEqual('Unit Tests', req.hdf['chart.title'])
-        self.assertEqual('', req.hdf['chart.data.0.0'])
-        self.assertEqual('Total', req.hdf['chart.data.1.0'])
-        self.assertEqual('Failures', req.hdf['chart.data.2.0'])
-
-    def test_single_platform(self):
-        config = Mock(name='trunk')
-        build = Build(self.env, config='trunk', platform=1, rev=123,
-                      rev_time=42)
-        build.insert()
-        report = Report(self.env, build=build.id, step='foo', category='test')
-        report.items += [{'status': 'success'}, {'status': 'failure'},
-                         {'status': 'success'}]
-        report.insert()
-
-        req = Mock(hdf=HDFWrapper())
-        generator = TestResultsChartGenerator(self.env)
-        template = generator.generate_chart_data(req, config, 'test')
-        self.assertEqual('bitten_chart_tests.cs', template)
-        self.assertEqual('Unit Tests', req.hdf['chart.title'])
-        self.assertEqual('', req.hdf['chart.data.0.0'])
-        self.assertEqual('[123]', req.hdf['chart.data.0.1'])
-        self.assertEqual('Total', req.hdf['chart.data.1.0'])
-        self.assertEqual('3', req.hdf['chart.data.1.1'])
-        self.assertEqual('Failures', req.hdf['chart.data.2.0'])
-        self.assertEqual('1', req.hdf['chart.data.2.1'])
-
-    def test_multi_platform(self):
-        config = Mock(name='trunk')
-
-        build = Build(self.env, config='trunk', platform=1, rev=123,
-                      rev_time=42)
-        build.insert()
-        report = Report(self.env, build=build.id, step='foo', category='test')
-        report.items += [{'status': 'success'}, {'status': 'failure'},
-                         {'status': 'success'}]
-        report.insert()
-
-        build = Build(self.env, config='trunk', platform=2, rev=123,
-                      rev_time=42)
-        build.insert()
-        report = Report(self.env, build=build.id, step='foo', category='test')
-        report.items += [{'status': 'success'}, {'status': 'failure'},
-                         {'status': 'failure'}]
-        report.insert()
-
-        req = Mock(hdf=HDFWrapper())
-        generator = TestResultsChartGenerator(self.env)
-        template = generator.generate_chart_data(req, config, 'test')
-        self.assertEqual('bitten_chart_tests.cs', template)
-        self.assertEqual('Unit Tests', req.hdf['chart.title'])
-        self.assertEqual('', req.hdf['chart.data.0.0'])
-        self.assertEqual('[123]', req.hdf['chart.data.0.1'])
-        self.assertEqual('Total', req.hdf['chart.data.1.0'])
-        self.assertEqual('3', req.hdf['chart.data.1.1'])
-        self.assertEqual('Failures', req.hdf['chart.data.2.0'])
-        self.assertEqual('2', req.hdf['chart.data.2.1'])
-
-
-class TestCoverageChartGeneratorTestCase(unittest.TestCase):
-
-    def setUp(self):
-        self.env = EnvironmentStub()
-        self.env.path = ''
-        db = self.env.get_db_cnx()
-        cursor = db.cursor()
-        for table in schema:
-            for stmt in schema_to_sql(self.env, db, table):
-                cursor.execute(stmt)
-
-    def test_supported_categories(self):
-        generator = TestCoverageChartGenerator(self.env)
-        self.assertEqual(['coverage'], generator.get_supported_categories())
-
-    def test_no_reports(self):
-        req = Mock(hdf=HDFWrapper())
-        config = Mock(name='trunk')
-        generator = TestCoverageChartGenerator(self.env)
-        template = generator.generate_chart_data(req, config, 'coverage')
-        self.assertEqual('bitten_chart_coverage.cs', template)
-        self.assertEqual('Test Coverage', req.hdf['chart.title'])
-        self.assertEqual('', req.hdf['chart.data.0.0'])
-        self.assertEqual('Lines of code', req.hdf['chart.data.1.0'])
-        self.assertEqual('Coverage', req.hdf['chart.data.2.0'])
-
-    def test_single_platform(self):
-        config = Mock(name='trunk')
-        build = Build(self.env, config='trunk', platform=1, rev=123,
-                      rev_time=42)
-        build.insert()
-        report = Report(self.env, build=build.id, step='foo',
-                        category='coverage')
-        report.items += [{'lines': '12', 'percentage': '25'}]
-        report.insert()
-
-        req = Mock(hdf=HDFWrapper())
-        generator = TestCoverageChartGenerator(self.env)
-        template = generator.generate_chart_data(req, config, 'coverage')
-        self.assertEqual('bitten_chart_coverage.cs', template)
-        self.assertEqual('Test Coverage', req.hdf['chart.title'])
-        self.assertEqual('', req.hdf['chart.data.0.0'])
-        self.assertEqual('[123]', req.hdf['chart.data.0.1'])
-        self.assertEqual('Lines of code', req.hdf['chart.data.1.0'])
-        self.assertEqual('12', req.hdf['chart.data.1.1'])
-        self.assertEqual('Coverage', req.hdf['chart.data.2.0'])
-        self.assertEqual('3', req.hdf['chart.data.2.1'])
-
-    def test_multi_platform(self):
-        config = Mock(name='trunk')
-        build = Build(self.env, config='trunk', platform=1, rev=123,
-                      rev_time=42)
-        build.insert()
-        report = Report(self.env, build=build.id, step='foo',
-                        category='coverage')
-        report.items += [{'lines': '12', 'percentage': '25'}]
-        report.insert()
-        build = Build(self.env, config='trunk', platform=2, rev=123,
-                      rev_time=42)
-        build.insert()
-        report = Report(self.env, build=build.id, step='foo',
-                        category='coverage')
-        report.items += [{'lines': '12', 'percentage': '50'}]
-        report.insert()
-
-        req = Mock(hdf=HDFWrapper())
-        generator = TestCoverageChartGenerator(self.env)
-        template = generator.generate_chart_data(req, config, 'coverage')
-        self.assertEqual('bitten_chart_coverage.cs', template)
-        self.assertEqual('Test Coverage', req.hdf['chart.title'])
-        self.assertEqual('', req.hdf['chart.data.0.0'])
-        self.assertEqual('[123]', req.hdf['chart.data.0.1'])
-        self.assertEqual('Lines of code', req.hdf['chart.data.1.0'])
-        self.assertEqual('12', req.hdf['chart.data.1.1'])
-        self.assertEqual('Coverage', req.hdf['chart.data.2.0'])
-        self.assertEqual('6', req.hdf['chart.data.2.1'])
-
-
-def suite():
-    suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(TestResultsChartGeneratorTestCase))
-    suite.addTest(unittest.makeSuite(TestCoverageChartGeneratorTestCase))
-    return suite
-
-if __name__ == '__main__':
-    unittest.main(defaultTest='suite')
--- a/bitten/trac_ext/web_ui.py
+++ b/bitten/trac_ext/web_ui.py
@@ -35,8 +35,8 @@
                          BuildLog, Report
 from bitten.queue import collect_changes
 from bitten.recipe import Recipe, InvalidRecipeError
-from bitten.trac_ext.api import ILogFormatter, IReportSummarizer
-from bitten.trac_ext.charts import ReportChartController
+from bitten.trac_ext.api import ILogFormatter, IReportChartGenerator, \
+                                IReportSummarizer
 from bitten.util import xmlio
 
 _status_label = {Build.PENDING: 'pending',
@@ -796,6 +796,35 @@
         return reports
 
 
+class ReportChartController(Component):
+    implements(IRequestHandler)
+
+    generators = ExtensionPoint(IReportChartGenerator)
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        match = re.match(r'/build/([\w.-]+)/chart/(\w+)', req.path_info)
+        if match:
+            req.args['config'] = match.group(1)
+            req.args['category'] = match.group(2)
+            return True
+
+    def process_request(self, req):
+        category = req.args.get('category')
+        config = BuildConfig.fetch(self.env, name=req.args.get('config'))
+
+        for generator in self.generators:
+            if category in generator.get_supported_categories():
+                template = generator.generate_chart_data(req, config,
+                                                         category)
+                break
+        else:
+            raise TracError('Unknown report category "%s"' % category)
+
+        return template, 'text/xml'
+
+
 class SourceFileLinkFormatter(Component):
     """Detects references to files in the build log and renders them as links
     to the repository browser."""
--- a/setup.py
+++ b/setup.py
@@ -46,8 +46,8 @@
             'bitten.main = bitten.trac_ext.main',
             'bitten.master = bitten.master',
             'bitten.web_ui = bitten.trac_ext.web_ui',
-            'bitten.summarizers = bitten.trac_ext.summarizers',
-            'bitten.charts = bitten.trac_ext.charts'
+            'bitten.testing = bitten.report.testing',
+            'bitten.coverage = bitten.report.coverage'
         ],
         'bitten.recipe_commands': [
             NS + 'sh#exec = bitten.build.shtools:exec_',
Copyright (C) 2012-2017 Edgewall Software