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:])) |