| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| 3 | |
|---|
| 4 | """ |
|---|
| 5 | Annotation for lint results |
|---|
| 6 | """ |
|---|
| 7 | |
|---|
| 8 | __docformat__ = 'restructuredtext en' |
|---|
| 9 | |
|---|
| 10 | from trac.core import Component, implements |
|---|
| 11 | from trac.mimeview.api import IHTMLPreviewAnnotator |
|---|
| 12 | from trac.resource import Resource |
|---|
| 13 | from trac.web.api import IRequestFilter |
|---|
| 14 | from trac.web.chrome import add_stylesheet, add_ctxtnav |
|---|
| 15 | from trac.web.chrome import ITemplateProvider |
|---|
| 16 | from bitten.model import BuildConfig, Build, Report |
|---|
| 17 | from genshi.builder import tag |
|---|
| 18 | from trac.util.text import unicode_urlencode |
|---|
| 19 | |
|---|
| 20 | |
|---|
| 21 | class LintAnnotator(Component): |
|---|
| 22 | """Annotation for lint results""" |
|---|
| 23 | |
|---|
| 24 | implements(IRequestFilter, IHTMLPreviewAnnotator, ITemplateProvider) |
|---|
| 25 | |
|---|
| 26 | env = log = None # filled py trac |
|---|
| 27 | |
|---|
| 28 | # IRequestFilter methods |
|---|
| 29 | |
|---|
| 30 | def pre_process_request(self, req, handler): |
|---|
| 31 | """unused""" |
|---|
| 32 | return handler |
|---|
| 33 | |
|---|
| 34 | def post_process_request(self, req, template, data, content_type): |
|---|
| 35 | """Adds a 'Lint' context navigation menu item in source view and |
|---|
| 36 | links to the annotation in report summary. |
|---|
| 37 | """ |
|---|
| 38 | if not 'BUILD_VIEW' in req.perm: |
|---|
| 39 | return template, data, content_type |
|---|
| 40 | resource = data and data.get('context') \ |
|---|
| 41 | and data.get('context').resource or None |
|---|
| 42 | if not resource or not isinstance(resource, Resource): |
|---|
| 43 | pass |
|---|
| 44 | elif resource.realm == 'source' and data.get('file') \ |
|---|
| 45 | and not req.args.get('annotate') == 'lint': |
|---|
| 46 | add_ctxtnav(req, 'Lint', |
|---|
| 47 | title='Annotate file with lint result ' |
|---|
| 48 | 'data (if available)', |
|---|
| 49 | href=req.href.browser(resource.id, |
|---|
| 50 | annotate='lint', rev=data.get('rev'))) |
|---|
| 51 | |
|---|
| 52 | elif resource.realm == 'build' and data.get('build', {}).get('steps'): |
|---|
| 53 | # in report summary, set link to lint annotation |
|---|
| 54 | steps = data['build']['steps'] |
|---|
| 55 | rev = data['build']['rev'] |
|---|
| 56 | for step in steps: |
|---|
| 57 | for report in step.get('reports', []): |
|---|
| 58 | if report.get('category') != 'lint': |
|---|
| 59 | continue |
|---|
| 60 | for item in report.get('data', {}).get('data', []): |
|---|
| 61 | href = item.get('href') |
|---|
| 62 | if not href or 'annotate' in href: |
|---|
| 63 | continue |
|---|
| 64 | sep = ('?' in href) and '&' or '?' |
|---|
| 65 | param = {'rev': rev, 'annotate': 'lint'} |
|---|
| 66 | href = href + sep + unicode_urlencode(param) |
|---|
| 67 | item['href'] = href + '#Lint1' |
|---|
| 68 | return template, data, content_type |
|---|
| 69 | |
|---|
| 70 | # IHTMLPreviewAnnotator methods |
|---|
| 71 | |
|---|
| 72 | def get_annotation_type(self): |
|---|
| 73 | """returns: type (css class), short name, long name (tip strip)""" |
|---|
| 74 | return 'lint', 'Lint', 'Lint results' |
|---|
| 75 | |
|---|
| 76 | itemid = 0 |
|---|
| 77 | |
|---|
| 78 | def get_annotation_data(self, context): |
|---|
| 79 | """add annotation data for lint""" |
|---|
| 80 | |
|---|
| 81 | context.perm.require('BUILD_VIEW') |
|---|
| 82 | |
|---|
| 83 | add_stylesheet(context.req, 'bitten/bitten_coverage.css') |
|---|
| 84 | add_stylesheet(context.req, 'bitten/bitten_lintannotator.css') |
|---|
| 85 | |
|---|
| 86 | resource = context.resource |
|---|
| 87 | |
|---|
| 88 | # attempt to use the version passed in with the request, |
|---|
| 89 | # otherwise fall back to the latest version of this file. |
|---|
| 90 | try: |
|---|
| 91 | version = context.req.args['rev'] |
|---|
| 92 | except (KeyError, TypeError): |
|---|
| 93 | version = resource.version |
|---|
| 94 | self.log.debug('no version passed to get_annotation_data') |
|---|
| 95 | |
|---|
| 96 | builds = Build.select(self.env, rev=version) |
|---|
| 97 | |
|---|
| 98 | self.log.debug("Looking for lint report for %s@%s [%s]..." % ( |
|---|
| 99 | resource.id, str(resource.version), version)) |
|---|
| 100 | |
|---|
| 101 | self.itemid = 0 |
|---|
| 102 | data = {} |
|---|
| 103 | reports = None |
|---|
| 104 | for build in builds: |
|---|
| 105 | config = BuildConfig.fetch(self.env, build.config) |
|---|
| 106 | if not resource.id.lstrip('/').startswith(config.path.lstrip('/')): |
|---|
| 107 | self.log.debug('Skip build %s' % build) |
|---|
| 108 | continue |
|---|
| 109 | path_in_config = resource.id[len(config.path)+1:].lstrip('/') |
|---|
| 110 | reports = Report.select(self.env, build=build.id, category='lint') |
|---|
| 111 | for report in reports: |
|---|
| 112 | for item in report.items: |
|---|
| 113 | if item.get('file') == path_in_config: |
|---|
| 114 | line = item.get('line') |
|---|
| 115 | if line: |
|---|
| 116 | problem = {'category': item.get('category', ''), |
|---|
| 117 | 'tag': item.get('tag', ''), |
|---|
| 118 | 'bid': build.id, |
|---|
| 119 | 'rbuild': report.build, |
|---|
| 120 | 'rstep': report.step, 'rid': report.id} |
|---|
| 121 | data.setdefault(int(line), []).append(problem) |
|---|
| 122 | if data: |
|---|
| 123 | self.log.debug("Lint annotate for %s@%s: %s results" % \ |
|---|
| 124 | (resource.id, resource.version, len(data))) |
|---|
| 125 | return data |
|---|
| 126 | if not builds: |
|---|
| 127 | self.log.debug("No builds found") |
|---|
| 128 | elif not reports: |
|---|
| 129 | self.log.debug("No reports found") |
|---|
| 130 | else: |
|---|
| 131 | self.log.debug("No item of any report matched (%s)" % reports) |
|---|
| 132 | return None |
|---|
| 133 | |
|---|
| 134 | def annotate_row(self, context, row, lineno, line, data): |
|---|
| 135 | "add column with Lint data to annotation" |
|---|
| 136 | if data == None: |
|---|
| 137 | row.append(tag.th()) |
|---|
| 138 | return |
|---|
| 139 | row_data = data.get(lineno, None) |
|---|
| 140 | if row_data == None: |
|---|
| 141 | row.append(tag.th(class_='covered')) |
|---|
| 142 | return |
|---|
| 143 | |
|---|
| 144 | self.log.debug('problems in line no %d:' % lineno) |
|---|
| 145 | categories = '' |
|---|
| 146 | problems = [] |
|---|
| 147 | for item in row_data: |
|---|
| 148 | categories += item['category'] and item['category'][0] or '-' |
|---|
| 149 | self.log.debug(' %s' % item) |
|---|
| 150 | problems.append('%(category)s: %(tag)s in report %(rid)d' % item) |
|---|
| 151 | problems = '\n'.join(problems) |
|---|
| 152 | self.itemid += 1 |
|---|
| 153 | row.append(tag.th(tag.a(categories, href='#Lint%d' % self.itemid), |
|---|
| 154 | class_='uncovered', title=problems, |
|---|
| 155 | id_='Lint%d' % self.itemid)) |
|---|
| 156 | self.log.debug('%s' % row) |
|---|
| 157 | |
|---|
| 158 | |
|---|
| 159 | |
|---|
| 160 | # ITemplateProvider methods |
|---|
| 161 | |
|---|
| 162 | def get_templates_dirs(self): |
|---|
| 163 | """unused""" |
|---|
| 164 | return [] |
|---|
| 165 | |
|---|
| 166 | def get_htdocs_dirs(self): |
|---|
| 167 | """ |
|---|
| 168 | Return a list of directories with static resources (such as style |
|---|
| 169 | sheets, images, etc.) |
|---|
| 170 | |
|---|
| 171 | Each item in the list must be a `(prefix, abspath)` tuple. The |
|---|
| 172 | `prefix` part defines the path in the URL that requests to these |
|---|
| 173 | resources are prefixed with. |
|---|
| 174 | |
|---|
| 175 | The `abspath` is the absolute path to the directory containing the |
|---|
| 176 | resources on the local file system. |
|---|
| 177 | """ |
|---|
| 178 | from pkg_resources import resource_filename |
|---|
| 179 | return [('bitten', resource_filename(__name__, 'htdocs'))] |
|---|