| 1 | # -*- coding: utf-8 -*- |
---|
| 2 | # |
---|
| 3 | # Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de> |
---|
| 4 | # Copyright (C) 2007-2010 Edgewall Software |
---|
| 5 | # All rights reserved. |
---|
| 6 | # |
---|
| 7 | # This software is licensed as described in the file COPYING, which |
---|
| 8 | # you should have received as part of this distribution. The terms |
---|
| 9 | # are also available at http://bitten.edgewall.org/wiki/License. |
---|
| 10 | |
---|
1 | 11 | from genshi.builder import tag |
---|
1 | 12 | from trac.core import * |
---|
1 | 13 | from trac.mimeview.api import IHTMLPreviewAnnotator |
---|
1 | 14 | from trac.resource import Resource |
---|
1 | 15 | from trac.util.datefmt import to_timestamp |
---|
1 | 16 | from trac.web.api import IRequestFilter |
---|
1 | 17 | from trac.web.chrome import add_stylesheet, add_ctxtnav, add_warning |
---|
1 | 18 | from bitten.api import IReportChartGenerator, IReportSummarizer |
---|
1 | 19 | from bitten.model import BuildConfig, Build, Report |
---|
| 20 | |
---|
1 | 21 | __docformat__ = 'restructuredtext en' |
---|
| 22 | |
---|
| 23 | |
---|
2 | 24 | class TestCoverageChartGenerator(Component): |
---|
1 | 25 | implements(IReportChartGenerator) |
---|
| 26 | |
---|
| 27 | # IReportChartGenerator methods |
---|
| 28 | |
---|
1 | 29 | def get_supported_categories(self): |
---|
1 | 30 | return ['coverage'] |
---|
| 31 | |
---|
1 | 32 | def generate_chart_data(self, req, config, category): |
---|
3 | 33 | assert category == 'coverage' |
---|
| 34 | |
---|
3 | 35 | db = self.env.get_db_cnx() |
---|
3 | 36 | cursor = db.cursor() |
---|
3 | 37 | cursor.execute(""" |
---|
3 | 38 | SELECT build.rev, SUM(%s) AS loc, SUM(%s * %s / 100) AS cov |
---|
3 | 39 | FROM bitten_build AS build |
---|
3 | 40 | LEFT OUTER JOIN bitten_report AS report ON (report.build=build.id) |
---|
3 | 41 | LEFT OUTER JOIN bitten_report_item AS item_lines |
---|
3 | 42 | ON (item_lines.report=report.id AND item_lines.name='lines') |
---|
3 | 43 | LEFT OUTER JOIN bitten_report_item AS item_percentage |
---|
3 | 44 | ON (item_percentage.report=report.id AND item_percentage.name='percentage' AND |
---|
3 | 45 | item_percentage.item=item_lines.item) |
---|
3 | 46 | WHERE build.config=%%s AND report.category='coverage' |
---|
3 | 47 | AND build.rev_time >= %%s AND build.rev_time <= %%s |
---|
3 | 48 | GROUP BY build.rev_time, build.rev, build.platform |
---|
3 | 49 | ORDER BY build.rev_time""" % (db.cast('item_lines.value', 'int'), |
---|
3 | 50 | db.cast('item_lines.value', 'int'), |
---|
3 | 51 | db.cast('item_percentage.value', 'int')), |
---|
3 | 52 | (config.name, |
---|
3 | 53 | config.min_rev_time(self.env), |
---|
3 | 54 | config.max_rev_time(self.env))) |
---|
| 55 | |
---|
3 | 56 | prev_rev = None |
---|
3 | 57 | coverage = [] |
---|
6 | 58 | for rev, loc, cov in cursor: |
---|
3 | 59 | if rev != prev_rev: |
---|
2 | 60 | coverage.append([rev, 0, 0]) |
---|
3 | 61 | if loc > coverage[-1][1]: |
---|
2 | 62 | coverage[-1][1] = int(loc) |
---|
3 | 63 | if cov > coverage[-1][2]: |
---|
3 | 64 | coverage[-1][2] = int(cov) |
---|
3 | 65 | prev_rev = rev |
---|
| 66 | |
---|
3 | 67 | data = {'title': 'Test Coverage', |
---|
3 | 68 | 'data': [ |
---|
5 | 69 | {'label': 'Lines of code', 'data': [[item[0], item[1]] for item in coverage], 'lines': {'fill': True}}, |
---|
5 | 70 | {'label': 'Coverage', 'data': [[item[0], item[2]] for item in coverage]}, |
---|
5 | 71 | ], |
---|
3 | 72 | 'options': { |
---|
3 | 73 | 'legend': {'position': 'sw', 'backgroundOpacity': 0.7}, |
---|
3 | 74 | 'xaxis': {'tickDecimals': 0}, |
---|
3 | 75 | 'yaxis': {'tickDecimals': 0}, |
---|
3 | 76 | }, |
---|
3 | 77 | } |
---|
3 | 78 | return 'json.txt', {"json": data} |
---|
| 79 | |
---|
| 80 | |
---|
2 | 81 | class TestCoverageSummarizer(Component): |
---|
1 | 82 | implements(IReportSummarizer) |
---|
| 83 | |
---|
| 84 | # IReportSummarizer methods |
---|
| 85 | |
---|
1 | 86 | def get_supported_categories(self): |
---|
1 | 87 | return ['coverage'] |
---|
| 88 | |
---|
1 | 89 | def render_summary(self, req, config, build, step, category): |
---|
0 | 90 | assert category == 'coverage' |
---|
| 91 | |
---|
0 | 92 | db = self.env.get_db_cnx() |
---|
0 | 93 | cursor = db.cursor() |
---|
0 | 94 | cursor.execute(""" |
---|
0 | 95 | SELECT item_name.value AS unit, item_file.value AS file, |
---|
0 | 96 | max(item_lines.value) AS loc, max(item_percentage.value) AS cov |
---|
0 | 97 | FROM bitten_report AS report |
---|
0 | 98 | LEFT OUTER JOIN bitten_report_item AS item_name |
---|
0 | 99 | ON (item_name.report=report.id AND item_name.name='name') |
---|
0 | 100 | LEFT OUTER JOIN bitten_report_item AS item_file |
---|
0 | 101 | ON (item_file.report=report.id AND item_file.item=item_name.item AND |
---|
0 | 102 | item_file.name='file') |
---|
0 | 103 | LEFT OUTER JOIN bitten_report_item AS item_lines |
---|
0 | 104 | ON (item_lines.report=report.id AND item_lines.item=item_name.item AND |
---|
0 | 105 | item_lines.name='lines') |
---|
0 | 106 | LEFT OUTER JOIN bitten_report_item AS item_percentage |
---|
0 | 107 | ON (item_percentage.report=report.id AND |
---|
0 | 108 | item_percentage.item=item_name.item AND |
---|
0 | 109 | item_percentage.name='percentage') |
---|
0 | 110 | WHERE category='coverage' AND build=%s AND step=%s |
---|
0 | 111 | GROUP BY file, item_name.value |
---|
0 | 112 | ORDER BY item_name.value""", (build.id, step.name)) |
---|
| 113 | |
---|
0 | 114 | units = [] |
---|
0 | 115 | total_loc, total_cov = 0, 0 |
---|
0 | 116 | for unit, file, loc, cov in cursor: |
---|
0 | 117 | try: |
---|
0 | 118 | loc, cov = int(loc), float(cov) |
---|
0 | 119 | except TypeError: |
---|
0 | 120 | continue # no rows |
---|
0 | 121 | if loc: |
---|
0 | 122 | d = {'name': unit, 'loc': loc, 'cov': int(cov)} |
---|
0 | 123 | if file: |
---|
0 | 124 | d['href'] = req.href.browser(config.path, file, rev=build.rev, annotate='coverage') |
---|
0 | 125 | units.append(d) |
---|
0 | 126 | total_loc += loc |
---|
0 | 127 | total_cov += loc * cov |
---|
| 128 | |
---|
0 | 129 | coverage = 0 |
---|
0 | 130 | if total_loc != 0: |
---|
0 | 131 | coverage = total_cov // total_loc |
---|
| 132 | |
---|
0 | 133 | return 'bitten_summary_coverage.html', { |
---|
0 | 134 | 'units': units, |
---|
0 | 135 | 'totals': {'loc': total_loc, 'cov': int(coverage)} |
---|
0 | 136 | } |
---|
| 137 | |
---|
| 138 | |
---|
2 | 139 | class TestCoverageAnnotator(Component): |
---|
| 140 | """ |
---|
| 141 | >>> from genshi.builder import tag |
---|
| 142 | >>> from trac.test import Mock, MockPerm |
---|
| 143 | >>> from trac.mimeview import Context |
---|
| 144 | >>> from trac.util.datefmt import to_datetime, utc |
---|
| 145 | >>> from trac.web.href import Href |
---|
| 146 | >>> from bitten.model import BuildConfig, Build, Report |
---|
| 147 | >>> from bitten.report.tests.coverage import env_stub_with_tables |
---|
| 148 | >>> env = env_stub_with_tables() |
---|
| 149 | >>> repos = Mock(get_changeset=lambda x: Mock(date=to_datetime(12345, utc))) |
---|
| 150 | >>> env.get_repository = lambda: repos |
---|
| 151 | |
---|
| 152 | >>> BuildConfig(env, name='trunk', path='trunk').insert() |
---|
| 153 | >>> Build(env, rev=123, config='trunk', rev_time=12345, platform=1).insert() |
---|
| 154 | >>> rpt = Report(env, build=1, step='test', category='coverage') |
---|
| 155 | >>> rpt.items.append({'file': 'foo.py', 'line_hits': '5 - 0'}) |
---|
| 156 | >>> rpt.insert() |
---|
| 157 | |
---|
| 158 | >>> ann = TestCoverageAnnotator(env) |
---|
| 159 | >>> req = Mock(href=Href('/'), perm=MockPerm(), |
---|
| 160 | ... chrome={'warnings': []}, args={}) |
---|
| 161 | |
---|
| 162 | Version in the branch should not match: |
---|
| 163 | >>> context = Context.from_request(req, 'source', '/branches/blah/foo.py', 123) |
---|
| 164 | >>> ann.get_annotation_data(context) |
---|
| 165 | [] |
---|
| 166 | |
---|
| 167 | Version in the trunk should match: |
---|
| 168 | >>> context = Context.from_request(req, 'source', '/trunk/foo.py', 123) |
---|
| 169 | >>> data = ann.get_annotation_data(context) |
---|
| 170 | >>> print data |
---|
| 171 | [u'5', u'-', u'0'] |
---|
| 172 | |
---|
| 173 | >>> def annotate_row(lineno, line): |
---|
| 174 | ... row = tag.tr() |
---|
| 175 | ... ann.annotate_row(context, row, lineno, line, data) |
---|
| 176 | ... return row.generate().render('html') |
---|
| 177 | |
---|
| 178 | >>> annotate_row(1, 'x = 1') |
---|
| 179 | '<tr><th class="covered">5</th></tr>' |
---|
| 180 | >>> annotate_row(2, '') |
---|
| 181 | '<tr><th></th></tr>' |
---|
| 182 | >>> annotate_row(3, 'y = x') |
---|
| 183 | '<tr><th class="uncovered">0</th></tr>' |
---|
1 | 184 | """ |
---|
1 | 185 | implements(IRequestFilter, IHTMLPreviewAnnotator) |
---|
| 186 | |
---|
| 187 | # IRequestFilter methods |
---|
| 188 | |
---|
1 | 189 | def pre_process_request(self, req, handler): |
---|
0 | 190 | return handler |
---|
| 191 | |
---|
1 | 192 | def post_process_request(self, req, template, data, content_type): |
---|
| 193 | """ Adds a 'Coverage' context navigation menu item. """ |
---|
0 | 194 | resource = data and data.get('context') \ |
---|
0 | 195 | and data.get('context').resource or None |
---|
0 | 196 | if resource and isinstance(resource, Resource) \ |
---|
0 | 197 | and resource.realm=='source' and data.get('file') \ |
---|
0 | 198 | and not req.args.get('annotate', '') == 'coverage': |
---|
0 | 199 | add_ctxtnav(req, tag.a('Coverage', |
---|
0 | 200 | title='Annotate file with test coverage ' |
---|
0 | 201 | 'data (if available)', |
---|
0 | 202 | href=req.href.browser(resource.id, |
---|
0 | 203 | annotate='coverage', rev=req.args.get('rev'), |
---|
0 | 204 | created=data.get('rev')), |
---|
0 | 205 | rel='nofollow')) |
---|
0 | 206 | return template, data, content_type |
---|
| 207 | |
---|
| 208 | # IHTMLPreviewAnnotator methods |
---|
| 209 | |
---|
1 | 210 | def get_annotation_type(self): |
---|
0 | 211 | return 'coverage', 'Cov', 'Code coverage' |
---|
| 212 | |
---|
1 | 213 | def get_annotation_data(self, context): |
---|
2 | 214 | add_stylesheet(context.req, 'bitten/bitten_coverage.css') |
---|
| 215 | |
---|
2 | 216 | resource = context.resource |
---|
| 217 | |
---|
| 218 | # attempt to use the version passed in with the request, |
---|
| 219 | # otherwise fall back to the latest version of this file. |
---|
2 | 220 | version = context.req.args.get('rev', resource.version) |
---|
| 221 | # get the last change revision for the file so that we can |
---|
| 222 | # pick coverage data as latest(version >= file_revision) |
---|
2 | 223 | created = context.req.args.get('created', resource.version) |
---|
| 224 | |
---|
2 | 225 | repos = self.env.get_repository() |
---|
2 | 226 | version_time = to_timestamp(repos.get_changeset(version).date) |
---|
2 | 227 | if version != created: |
---|
0 | 228 | created_time = to_timestamp(repos.get_changeset(created).date) |
---|
0 | 229 | else: |
---|
2 | 230 | created_time = version_time |
---|
| 231 | |
---|
2 | 232 | self.log.debug("Looking for coverage report for %s@%s [%s:%s]..." % ( |
---|
2 | 233 | resource.id, str(resource.version), created, version)) |
---|
| 234 | |
---|
2 | 235 | db = self.env.get_db_cnx() |
---|
2 | 236 | cursor = db.cursor() |
---|
2 | 237 | cursor.execute(""" |
---|
2 | 238 | SELECT b.id, b.rev, i2.value |
---|
2 | 239 | FROM bitten_config AS c |
---|
2 | 240 | INNER JOIN bitten_build AS b ON c.name=b.config |
---|
2 | 241 | INNER JOIN bitten_report AS r ON b.id=r.build |
---|
2 | 242 | INNER JOIN bitten_report_item AS i1 ON r.id=i1.report |
---|
2 | 243 | INNER JOIN bitten_report_item AS i2 ON (i1.item=i2.item |
---|
2 | 244 | AND i1.report=i2.report) |
---|
2 | 245 | WHERE i2.name='line_hits' |
---|
2 | 246 | AND b.rev_time>=%s |
---|
2 | 247 | AND b.rev_time<=%s |
---|
2 | 248 | AND i1.name='file' |
---|
2 | 249 | AND """ + db.concat('c.path', "'/'", 'i1.value') + """=%s |
---|
2 | 250 | ORDER BY b.rev_time DESC LIMIT 1""" , |
---|
2 | 251 | (created_time, version_time, resource.id.lstrip('/'))) |
---|
| 252 | |
---|
2 | 253 | row = cursor.fetchone() |
---|
2 | 254 | if row: |
---|
1 | 255 | build_id, build_rev, line_hits = row |
---|
1 | 256 | coverage = line_hits.split() |
---|
1 | 257 | self.log.debug("Coverage annotate for %s@%s using build %d: %s", |
---|
1 | 258 | resource.id, build_rev, build_id, coverage) |
---|
1 | 259 | return coverage |
---|
1 | 260 | add_warning(context.req, "No coverage annotation found for " |
---|
1 | 261 | "/%s for revision range [%s:%s]." % ( |
---|
1 | 262 | resource.id.lstrip('/'), version, created)) |
---|
1 | 263 | return [] |
---|
| 264 | |
---|
1 | 265 | def annotate_row(self, context, row, lineno, line, data): |
---|
3 | 266 | from genshi.builder import tag |
---|
3 | 267 | lineno -= 1 # 0-based index for data |
---|
3 | 268 | if lineno >= len(data): |
---|
0 | 269 | row.append(tag.th()) |
---|
0 | 270 | return |
---|
3 | 271 | row_data = data[lineno] |
---|
3 | 272 | if row_data == '-': |
---|
1 | 273 | row.append(tag.th()) |
---|
2 | 274 | elif row_data == '0': |
---|
1 | 275 | row.append(tag.th(row_data, class_='uncovered')) |
---|
1 | 276 | else: |
---|
1 | 277 | row.append(tag.th(row_data, class_='covered')) |
---|