Gentoo Archives: gentoo-commits

From: "Michał Górny" <mgorny@g.o>
To: gentoo-commits@l.g.o
Subject: [gentoo-commits] proj/qa-scripts:master commit in: pkgcheck2html/
Date: Sun, 04 Dec 2016 08:50:12
Message-Id: 1480841387.cce1dfea834ae526ebbe8506fdad19cc03287730.mgorny@gentoo
1 commit: cce1dfea834ae526ebbe8506fdad19cc03287730
2 Author: Michał Górny <mgorny <AT> gentoo <DOT> org>
3 AuthorDate: Sat Dec 3 20:56:23 2016 +0000
4 Commit: Michał Górny <mgorny <AT> gentoo <DOT> org>
5 CommitDate: Sun Dec 4 08:49:47 2016 +0000
6 URL: https://gitweb.gentoo.org/proj/qa-scripts.git/commit/?id=cce1dfea
7
8 Add pkgcheck XML output to HTML formatter scripts
9
10 pkgcheck2html/jinja2htmlcompress.py | 150 +++++++++++++++++++++++++++
11 pkgcheck2html/output.css | 184 ++++++++++++++++++++++++++++++++++
12 pkgcheck2html/output.html.jinja | 67 +++++++++++++
13 pkgcheck2html/pkgcheck2html.conf.json | 26 +++++
14 pkgcheck2html/pkgcheck2html.py | 139 +++++++++++++++++++++++++
15 5 files changed, 566 insertions(+)
16
17 diff --git a/pkgcheck2html/jinja2htmlcompress.py b/pkgcheck2html/jinja2htmlcompress.py
18 new file mode 100644
19 index 0000000..5dfb211
20 --- /dev/null
21 +++ b/pkgcheck2html/jinja2htmlcompress.py
22 @@ -0,0 +1,150 @@
23 +# -*- coding: utf-8 -*-
24 +"""
25 + jinja2htmlcompress
26 + ~~~~~~~~~~~~~~~~~~
27 +
28 + A Jinja2 extension that eliminates useless whitespace at template
29 + compilation time without extra overhead.
30 +
31 + :copyright: (c) 2011 by Armin Ronacher.
32 + :license: BSD, see LICENSE for more details.
33 +"""
34 +import re
35 +from jinja2.ext import Extension
36 +from jinja2.lexer import Token, describe_token
37 +from jinja2 import TemplateSyntaxError
38 +
39 +
40 +_tag_re = re.compile(r'(?:<(/?)([a-zA-Z0-9_-]+)\s*|(>\s*))(?s)')
41 +_ws_normalize_re = re.compile(r'[ \t\r\n]+')
42 +
43 +
44 +class StreamProcessContext(object):
45 +
46 + def __init__(self, stream):
47 + self.stream = stream
48 + self.token = None
49 + self.stack = []
50 +
51 + def fail(self, message):
52 + raise TemplateSyntaxError(message, self.token.lineno,
53 + self.stream.name, self.stream.filename)
54 +
55 +
56 +def _make_dict_from_listing(listing):
57 + rv = {}
58 + for keys, value in listing:
59 + for key in keys:
60 + rv[key] = value
61 + return rv
62 +
63 +
64 +class HTMLCompress(Extension):
65 + isolated_elements = set(['script', 'style', 'noscript', 'textarea'])
66 + void_elements = set(['br', 'img', 'area', 'hr', 'param', 'input',
67 + 'embed', 'col'])
68 + block_elements = set(['div', 'p', 'form', 'ul', 'ol', 'li', 'table', 'tr',
69 + 'tbody', 'thead', 'tfoot', 'tr', 'td', 'th', 'dl',
70 + 'dt', 'dd', 'blockquote', 'h1', 'h2', 'h3', 'h4',
71 + 'h5', 'h6', 'pre'])
72 + breaking_rules = _make_dict_from_listing([
73 + (['p'], set(['#block'])),
74 + (['li'], set(['li'])),
75 + (['td', 'th'], set(['td', 'th', 'tr', 'tbody', 'thead', 'tfoot'])),
76 + (['tr'], set(['tr', 'tbody', 'thead', 'tfoot'])),
77 + (['thead', 'tbody', 'tfoot'], set(['thead', 'tbody', 'tfoot'])),
78 + (['dd', 'dt'], set(['dl', 'dt', 'dd']))
79 + ])
80 +
81 + def is_isolated(self, stack):
82 + for tag in reversed(stack):
83 + if tag in self.isolated_elements:
84 + return True
85 + return False
86 +
87 + def is_breaking(self, tag, other_tag):
88 + breaking = self.breaking_rules.get(other_tag)
89 + return breaking and (tag in breaking or
90 + ('#block' in breaking and tag in self.block_elements))
91 +
92 + def enter_tag(self, tag, ctx):
93 + while ctx.stack and self.is_breaking(tag, ctx.stack[-1]):
94 + self.leave_tag(ctx.stack[-1], ctx)
95 + if tag not in self.void_elements:
96 + ctx.stack.append(tag)
97 +
98 + def leave_tag(self, tag, ctx):
99 + if not ctx.stack:
100 + ctx.fail('Tried to leave "%s" but something closed '
101 + 'it already' % tag)
102 + if tag == ctx.stack[-1]:
103 + ctx.stack.pop()
104 + return
105 + for idx, other_tag in enumerate(reversed(ctx.stack)):
106 + if other_tag == tag:
107 + for num in xrange(idx + 1):
108 + ctx.stack.pop()
109 + elif not self.breaking_rules.get(other_tag):
110 + break
111 +
112 + def normalize(self, ctx):
113 + pos = 0
114 + buffer = []
115 + def write_data(value):
116 + if not self.is_isolated(ctx.stack):
117 + value = _ws_normalize_re.sub(' ', value.strip())
118 + buffer.append(value)
119 +
120 + for match in _tag_re.finditer(ctx.token.value):
121 + closes, tag, sole = match.groups()
122 + preamble = ctx.token.value[pos:match.start()]
123 + write_data(preamble)
124 + if sole:
125 + write_data(sole)
126 + else:
127 + buffer.append(match.group())
128 + (closes and self.leave_tag or self.enter_tag)(tag, ctx)
129 + pos = match.end()
130 +
131 + write_data(ctx.token.value[pos:])
132 + return u''.join(buffer)
133 +
134 + def filter_stream(self, stream):
135 + ctx = StreamProcessContext(stream)
136 + for token in stream:
137 + if token.type != 'data':
138 + yield token
139 + continue
140 + ctx.token = token
141 + value = self.normalize(ctx)
142 + yield Token(token.lineno, 'data', value)
143 +
144 +
145 +class SelectiveHTMLCompress(HTMLCompress):
146 +
147 + def filter_stream(self, stream):
148 + ctx = StreamProcessContext(stream)
149 + strip_depth = 0
150 + while 1:
151 + if stream.current.type == 'block_begin':
152 + if stream.look().test('name:strip') or \
153 + stream.look().test('name:endstrip'):
154 + stream.skip()
155 + if stream.current.value == 'strip':
156 + strip_depth += 1
157 + else:
158 + strip_depth -= 1
159 + if strip_depth < 0:
160 + ctx.fail('Unexpected tag endstrip')
161 + stream.skip()
162 + if stream.current.type != 'block_end':
163 + ctx.fail('expected end of block, got %s' %
164 + describe_token(stream.current))
165 + stream.skip()
166 + if strip_depth > 0 and stream.current.type == 'data':
167 + ctx.token = stream.current
168 + value = self.normalize(ctx)
169 + yield Token(stream.current.lineno, 'data', value)
170 + else:
171 + yield stream.current
172 + stream.next()
173
174 diff --git a/pkgcheck2html/output.css b/pkgcheck2html/output.css
175 new file mode 100644
176 index 0000000..6888102
177 --- /dev/null
178 +++ b/pkgcheck2html/output.css
179 @@ -0,0 +1,184 @@
180 +/* (c) 2016 Michał Górny, Patrice Clement */
181 +/* 2-clause BSD license */
182 +
183 +*
184 +{
185 + box-sizing: border-box;
186 +}
187 +
188 +body
189 +{
190 + margin: 0;
191 + background-color: #463C65;
192 + font-family: sans-serif;
193 + font-size: 14px;
194 +}
195 +
196 +address
197 +{
198 + color: white;
199 + text-align: center;
200 + margin: 1em;
201 +}
202 +
203 +.nav
204 +{
205 + width: 20%;
206 + position: absolute;
207 + top: 0;
208 +}
209 +
210 +.nav ul
211 +{
212 + list-style: none;
213 + margin: 0;
214 + padding: 1%;
215 +}
216 +
217 +.nav li
218 +{
219 + padding: 0 1em;
220 + border-radius: 4px;
221 + margin-bottom: .3em;
222 + background-color: #62548F;
223 +}
224 +
225 +.nav li a
226 +{
227 + display: block;
228 + width: 100%;
229 +}
230 +
231 +.nav h2
232 +{
233 + color: white;
234 + text-align: center;
235 + font-size: 300%;
236 + font-weight: bold;
237 + text-transform: uppercase;
238 + font-family: serif;
239 +}
240 +
241 +ul.nav li.header
242 +{
243 + background-color: #463C65;
244 +}
245 +
246 +.content, h1
247 +{
248 + padding: 2%;
249 + margin: 0 0 0 20%;
250 + background-color: #DDDAEC;
251 +}
252 +
253 +h1 {
254 + font-family: serif;
255 + color: #23457F;
256 + font-size: 400%;
257 + line-height: 2em;
258 + font-weight: bold;
259 + text-transform: uppercase;
260 + text-align: center;
261 + letter-spacing: .15em;
262 +}
263 +
264 +th
265 +{
266 + text-align: left;
267 + padding: 1em 0;
268 + color: #23457F;
269 +}
270 +
271 +th.h2
272 +{
273 + font-size: 120%;
274 +}
275 +
276 +th.h3
277 +{
278 + padding-left: 1em;
279 + font-size: 110%;
280 +}
281 +
282 +th:target
283 +{
284 + background-color: #dfd;
285 +}
286 +
287 +th small
288 +{
289 + padding-left: .5em;
290 + visibility: hidden;
291 +}
292 +
293 +th:hover small
294 +{
295 + visibility: visible;
296 +}
297 +
298 +td
299 +{
300 + background-color: white;
301 + line-height: 2em;
302 + font-size: 120%;
303 + padding-left: .5em;
304 + white-space: pre-wrap;
305 +}
306 +
307 +td:hover
308 +{
309 + background-color: #eee;
310 +}
311 +
312 +tr.err td
313 +{
314 + background-color: #7E0202;
315 + color: white;
316 +}
317 +
318 +tr.err td:hover
319 +{
320 + background-color: #DA0404;
321 +}
322 +
323 +tr.warn td
324 +{
325 + background-color: orange;
326 +}
327 +
328 +tr.warn td:hover
329 +{
330 + background-color: #FFBB3E;
331 +}
332 +
333 +.nav a
334 +{
335 + font-size: 150%;
336 + line-height: 1.5em;
337 + text-decoration: none;
338 + white-space: pre;
339 + overflow: hidden;
340 + text-overflow: ellipsis;
341 +}
342 +
343 +.warn a
344 +{
345 + color: orange;
346 +}
347 +
348 +.err a
349 +{
350 + color: #F06F74;
351 +}
352 +
353 +.nav li:hover
354 +{
355 + min-width: 100%;
356 + width: -moz-max-content;
357 + width: max-content;
358 +}
359 +
360 +.nav a:hover
361 +{
362 + color: white;
363 +}
364
365 diff --git a/pkgcheck2html/output.html.jinja b/pkgcheck2html/output.html.jinja
366 new file mode 100644
367 index 0000000..2e44619
368 --- /dev/null
369 +++ b/pkgcheck2html/output.html.jinja
370 @@ -0,0 +1,67 @@
371 +<!DOCTYPE html>
372 +<html>
373 + <head>
374 + <meta charset="utf-8"/>
375 + <title>Gentoo CI - QA check results</title>
376 + <link rel="stylesheet" type="text/css" href="output.css" />
377 + </head>
378 +
379 + <body>
380 + <h1>QA check results</h1>
381 +
382 + {% if errors or warnings %}
383 + <div class="nav">
384 + <h2>issues</h2>
385 +
386 + <ul>
387 + {% for g in errors %}
388 + <li class="err"><a href="#{{ g|join('/') }}">{{ g|join('/') }}</a></li>
389 + {% endfor %}
390 + {% for g in warnings %}
391 + <li class="warn"><a href="#{{ g|join('/') }}">{{ g|join('/') }}</a></li>
392 + {% endfor %}
393 + </ul>
394 + </div>
395 + {% endif %}
396 +
397 + <div class="content">
398 + <table>
399 + {% for g, r in results %}
400 + {% set h2_id = g[0] if g else "global" %}
401 + <tr><th colspan="3" class="h2" id="{{ h2_id }}">
402 + {{ g[0] if g else "Global-scope results" }}
403 + <small><a href="#{{ h2_id }}">¶</a></small>
404 + </th></tr>
405 +
406 + {% for g, r in r %}
407 + {% if g[0] %}
408 + {% set h3_id = g[0] + "/" + g[1] if g[1] else "_cat" %}
409 + <tr><th colspan="3" class="h3" id="{{ h3_id }}">
410 + {{ g[1] if g[1] else "Category results" }}
411 + <small><a href="#{{ h3_id }}">¶</a></small>
412 + </th></tr>
413 + {% endif %}
414 +
415 + {% for g, r in r %}
416 + {% for rx in r %}
417 + {% set class_str = "" %}
418 + {% if rx.css_class %}
419 + {% set class_str = ' class="' + rx.css_class + '"' %}
420 + {% endif %}
421 + <tr{{ class_str }}>
422 + <td>{{ g[2] if loop.index == 1 else "" }}</td>
423 + <td>{{ rx.class }}</td>
424 + <td>{{ rx.msg|escape }}</td>
425 + </tr>
426 + {% endfor %}
427 + {% endfor %}
428 + {% endfor %}
429 + {% endfor %}
430 + </table>
431 + </div>
432 +
433 + <address>Generated based on results from: {{ ts.strftime("%F %T UTC") }}</address>
434 + </body>
435 +</html>
436 +
437 +<!-- vim:se ft=jinja : -->
438
439 diff --git a/pkgcheck2html/pkgcheck2html.conf.json b/pkgcheck2html/pkgcheck2html.conf.json
440 new file mode 100644
441 index 0000000..f9c597e
442 --- /dev/null
443 +++ b/pkgcheck2html/pkgcheck2html.conf.json
444 @@ -0,0 +1,26 @@
445 +{
446 + "CatMetadataXmlInvalidPkgRef": "err",
447 + "VisibleVcsPkg": "err",
448 + "MissingUri": "warn",
449 + "CatBadlyFormedXml": "err",
450 + "Glep31Violation": "err",
451 + "PkgBadlyFormedXml": "err",
452 + "CatInvalidXml": "err",
453 + "CatMetadataXmlInvalidCatRef": "err",
454 + "PkgMetadataXmlInvalidCatRef": "err",
455 + "ConflictingChksums": "err",
456 + "MissingChksum": "warn",
457 + "MissingManifest": "err",
458 + "CrappyDescription": "warn",
459 + "PkgMetadataXmlInvalidPkgRef": "err",
460 + "PkgMetadataXmlInvalidProjectError": "err",
461 + "PkgInvalidXml": "err",
462 + "NonsolvableDeps": "err",
463 + "UnusedLocalFlags": "err",
464 + "MetadataLoadError": "err",
465 + "UnknownManifest": "err",
466 + "NoFinalNewline": "err",
467 + "UnstatedIUSE": "err",
468 + "MetadataError": "err",
469 + "WrongIndentFound": "err"
470 +}
471
472 diff --git a/pkgcheck2html/pkgcheck2html.py b/pkgcheck2html/pkgcheck2html.py
473 new file mode 100755
474 index 0000000..466d8c1
475 --- /dev/null
476 +++ b/pkgcheck2html/pkgcheck2html.py
477 @@ -0,0 +1,139 @@
478 +#!/usr/bin/env python
479 +# vim:se fileencoding=utf8 :
480 +# (c) 2015-2016 Michał Górny
481 +# 2-clause BSD license
482 +
483 +import argparse
484 +import datetime
485 +import io
486 +import json
487 +import os
488 +import os.path
489 +import sys
490 +import xml.etree.ElementTree
491 +
492 +import jinja2
493 +
494 +
495 +class Result(object):
496 + def __init__(self, el, class_mapping):
497 + self._el = el
498 + self._class_mapping = class_mapping
499 +
500 + def __getattr__(self, key):
501 + return self._el.findtext(key) or ''
502 +
503 + @property
504 + def css_class(self):
505 + return self._class_mapping.get(getattr(self, 'class'), '')
506 +
507 +
508 +def result_sort_key(r):
509 + return (r.category, r.package, r.version, getattr(r, 'class'), r.msg)
510 +
511 +
512 +def get_results(input_paths, class_mapping):
513 + for input_path in input_paths:
514 + checks = xml.etree.ElementTree.parse(input_path).getroot()
515 + for r in checks:
516 + yield Result(r, class_mapping)
517 +
518 +
519 +def split_result_group(it):
520 + for r in it:
521 + if not r.category:
522 + yield ((), r)
523 + elif not r.package:
524 + yield ((r.category,), r)
525 + elif not r.version:
526 + yield ((r.category, r.package), r)
527 + else:
528 + yield ((r.category, r.package, r.version), r)
529 +
530 +
531 +def group_results(it, level = 3):
532 + prev_group = ()
533 + prev_l = []
534 +
535 + for g, r in split_result_group(it):
536 + if g[:level] != prev_group:
537 + if prev_l:
538 + yield (prev_group, prev_l)
539 + prev_group = g[:level]
540 + prev_l = []
541 + prev_l.append(r)
542 + yield (prev_group, prev_l)
543 +
544 +
545 +def deep_group(it, level = 1):
546 + for g, r in group_results(it, level):
547 + if level > 3:
548 + for x in r:
549 + yield x
550 + else:
551 + yield (g, deep_group(r, level+1))
552 +
553 +
554 +def find_of_class(it, cls, level = 2):
555 + for g, r in group_results(it, level):
556 + for x in r:
557 + if x.css_class == cls:
558 + yield g
559 + break
560 +
561 +
562 +def get_result_timestamp(paths):
563 + for p in paths:
564 + st = os.stat(p)
565 + return datetime.datetime.utcfromtimestamp(st.st_mtime)
566 +
567 +
568 +def main(*args):
569 + p = argparse.ArgumentParser()
570 + p.add_argument('-o', '--output', default='-',
571 + help='Output HTML file ("-" for stdout)')
572 + p.add_argument('-t', '--timestamp', default=None,
573 + help='Timestamp for results (git ISO8601-like UTC)')
574 + p.add_argument('files', nargs='+',
575 + help='Input XML files')
576 + args = p.parse_args(args)
577 +
578 + conf_path = os.path.join(os.path.dirname(__file__), 'pkgcheck2html.conf.json')
579 + with io.open(conf_path, 'r', encoding='utf8') as f:
580 + class_mapping = json.load(f)
581 +
582 + jenv = jinja2.Environment(
583 + loader=jinja2.FileSystemLoader(os.path.dirname(__file__)),
584 + extensions=['jinja2htmlcompress.HTMLCompress'])
585 + t = jenv.get_template('output.html.jinja')
586 +
587 + results = sorted(get_results(args.files, class_mapping), key=result_sort_key)
588 +
589 + types = {}
590 + for r in results:
591 + cl = getattr(r, 'class')
592 + if cl not in types:
593 + types[cl] = 0
594 + types[cl] += 1
595 +
596 + if args.timestamp is not None:
597 + ts = datetime.datetime.strptime(args.timestamp, '%Y-%m-%d %H:%M:%S')
598 + else:
599 + ts = get_result_timestamp(args.files)
600 +
601 + out = t.render(
602 + results = deep_group(results),
603 + warnings = list(find_of_class(results, 'warn')),
604 + errors = list(find_of_class(results, 'err')),
605 + ts = ts,
606 + )
607 +
608 + if args.output == '-':
609 + sys.stdout.write(out)
610 + else:
611 + with io.open(args.output, 'w', encoding='utf8') as f:
612 + f.write(out)
613 +
614 +
615 +if __name__ == '__main__':
616 + sys.exit(main(*sys.argv[1:]))