# HG changeset patch # User mgood # Date 1205992770 0 # Node ID 3386b22da07b98fc8730210eb26a94c2916ce1b6 # Parent 9514fad39d60dd6f2869aa483c279ed0aadd7104 code coverage annotator for source browser diff --git a/bitten/htdocs/bitten_coverage.css b/bitten/htdocs/bitten_coverage.css new file mode 100644 --- /dev/null +++ b/bitten/htdocs/bitten_coverage.css @@ -0,0 +1,4 @@ +/* Code coverage file annotations */ +table.code th.coverage { width: 4em; } +table.code th.covered { background-color: #0f0; } +table.code th.uncovered { background-color: #f00; } diff --git a/bitten/report/coverage.py b/bitten/report/coverage.py --- a/bitten/report/coverage.py +++ b/bitten/report/coverage.py @@ -9,9 +9,11 @@ # are also available at http://bitten.edgewall.org/wiki/License. from trac.core import * -from trac.web.chrome import Chrome +from trac.mimeview.api import IHTMLPreviewAnnotator +from trac.web.chrome import Chrome, add_stylesheet from trac.web.clearsilver import HDFWrapper from bitten.api import IReportChartGenerator, IReportSummarizer +from bitten.model import BuildConfig, Build, Report __docformat__ = 'restructuredtext en' @@ -109,7 +111,7 @@ if loc: d = {'name': unit, 'loc': loc, 'cov': int(cov)} if file: - d['href'] = req.href.browser(config.path, file) + d['href'] = req.href.browser(config.path, file, rev=build.rev, annotate='coverage') data.append(d) total_loc += loc total_cov += loc * cov @@ -122,3 +124,90 @@ hdf['data'] = data hdf['totals'] = {'loc': total_loc, 'cov': int(coverage)} return hdf.render('bitten_summary_coverage.cs') + + +# Coverage annotation requires the new interface from 0.11 +if hasattr(IHTMLPreviewAnnotator, 'get_annotation_data'): + class TestCoverageAnnotator(Component): + """ + >>> from genshi.builder import tag + >>> from trac.test import Mock, MockPerm + >>> from trac.mimeview import Context + >>> from trac.web.href import Href + >>> from bitten.model import BuildConfig, Build, Report + >>> from bitten.report.tests.coverage import env_stub_with_tables + >>> env = env_stub_with_tables() + + >>> BuildConfig(env, name='trunk', path='trunk').insert() + >>> Build(env, rev=123, config='trunk', rev_time=12345, platform=1).insert() + >>> rpt = Report(env, build=1, step='test', category='coverage') + >>> rpt.items.append({'file': 'foo.py', 'line_hits': '5 - 0'}) + >>> rpt.insert() + + >>> ann = TestCoverageAnnotator(env) + >>> req = Mock(href=Href('/'), perm=MockPerm(), chrome={}) + + Version in the branch should not match: + >>> context = Context.from_request(req, 'source', 'branches/blah/foo.py', 123) + >>> ann.get_annotation_data(context) + [] + + Version in the trunk should match: + >>> context = Context.from_request(req, 'source', 'trunk/foo.py', 123) + >>> data = ann.get_annotation_data(context) + >>> print data + [u'5', u'-', u'0'] + + >>> def annotate_row(lineno, line): + ... row = tag.tr() + ... ann.annotate_row(context, row, lineno, line, data) + ... return row.generate().render('html') + + >>> annotate_row(1, 'x = 1') + '5' + >>> annotate_row(2, '') + '' + >>> annotate_row(3, 'y = x') + '0' + """ + implements(IHTMLPreviewAnnotator) + + # IHTMLPreviewAnnotator methods + + def get_annotation_type(self): + return 'coverage', 'Cov', 'Code coverage' + + def get_annotation_data(self, context): + add_stylesheet(context.req, 'bitten/bitten_coverage.css') + + resource = context.resource + builds = Build.select(self.env, rev=resource.version) + reports = [] + for build in builds: + config = BuildConfig.fetch(self.env, build.config) + if not resource.id.startswith(config.path): + continue + reports = Report.select(self.env, build=build.id, + category='coverage') + path_in_config = resource.id[len(config.path):].lstrip('/') + for report in reports: + for item in report.items: + if item.get('file') == path_in_config: + # TODO should aggregate coverage across builds + return item.get('line_hits', '').split() + return [] + + def annotate_row(self, context, row, lineno, line, data): + self.log.debug('%s', data) + from genshi.builder import tag + lineno -= 1 # 0-based index for data + if lineno >= len(data): + row.append(tag.th()) + return + row_data = data[lineno] + if row_data == '-': + row.append(tag.th()) + elif row_data == '0': + row.append(tag.th(row_data, class_='uncovered')) + else: + row.append(tag.th(row_data, class_='covered')) diff --git a/bitten/report/tests/coverage.py b/bitten/report/tests/coverage.py --- a/bitten/report/tests/coverage.py +++ b/bitten/report/tests/coverage.py @@ -8,27 +8,31 @@ # you should have received as part of this distribution. The terms # are also available at http://bitten.edgewall.org/wiki/License. +import doctest import unittest from trac.db import DatabaseManager from trac.test import EnvironmentStub, Mock from trac.web.clearsilver import HDFWrapper from bitten.model import * +from bitten.report import coverage from bitten.report.coverage import TestCoverageChartGenerator +def env_stub_with_tables(): + env = EnvironmentStub() + db = env.get_db_cnx() + cursor = db.cursor() + connector, _ = DatabaseManager(env)._get_connector() + for table in schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + return env class TestCoverageChartGeneratorTestCase(unittest.TestCase): def setUp(self): - self.env = EnvironmentStub() + self.env = env_stub_with_tables() self.env.path = '' - db = self.env.get_db_cnx() - cursor = db.cursor() - - connector, _ = DatabaseManager(self.env)._get_connector() - for table in schema: - for stmt in connector.to_sql(table): - cursor.execute(stmt) def test_supported_categories(self): generator = TestCoverageChartGenerator(self.env) @@ -100,6 +104,7 @@ def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestCoverageChartGeneratorTestCase)) + suite.addTest(doctest.DocTestSuite(coverage)) return suite if __name__ == '__main__':