:p
atchew
Login
These patches add template tags and filters designed for use in email sent by Patchew: convert ANSI to text, and grep with context. Not very much tested. :) Paolo Bonzini (6): ansi2html: add _write_form_feed ansi2html: create ANSIProcessor superclass ansi2html: create ANSI to text converter add custom template tags for manipulating testing logs testing: add reverse_testing_log testing: pass log URL in the TestingReport event mods/testing.py | 54 +++++----- patchew/logviewer.py | 252 ++++++++++++++++++++++++++++------------------ patchew/settings.py | 3 + patchew/tags.py | 96 ++++++++++++++++++ tests/test_ansi2html.py | 75 +++++++++++++- tests/test_custom_tags.py | 129 ++++++++++++++++++++++++ 6 files changed, 485 insertions(+), 124 deletions(-) create mode 100644 patchew/tags.py create mode 100755 tests/test_custom_tags.py -- 2.14.3
Form feeds are rendered differently by HTML and text converters, so move the HTML rendering out of _write_line's argument. Signed-off-by: Paolo Bonzini <pbonzini@redhat.com> --- patchew/logviewer.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/patchew/logviewer.py b/patchew/logviewer.py index XXXXXXX..XXXXXXX 100644 --- a/patchew/logviewer.py +++ b/patchew/logviewer.py @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLConverter(object): # the remaining light colors: dark grey and white self.bg = arg - 92 + def _write_form_feed(self): + yield '<hr>' + def _class_to_id(self, html_class): class_id = self.class_to_id.get(html_class, None) if class_id is None: @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLConverter(object): if csi[-1] == 'J': save_pos = self.pos - yield from self._write_line('<hr>') + yield from self._write_line('') + yield from self._write_form_feed() self._set_pos(save_pos) elif csi[-1] == 'K': self._parse_csi_with_args(csi, self._do_csi_K) @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLConverter(object): yield from self._write_line('\n') continue elif seq == '\f': - yield from self._write_line('\n<hr>') + yield from self._write_line('\n') + yield from self._write_form_feed() continue if self.lazy_contents != '': -- 2.14.3
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com> --- patchew/logviewer.py | 216 ++++++++++++++++++++++++++++----------------------- 1 file changed, 120 insertions(+), 96 deletions(-) diff --git a/patchew/logviewer.py b/patchew/logviewer.py index XXXXXXX..XXXXXXX 100644 --- a/patchew/logviewer.py +++ b/patchew/logviewer.py @@ -XXX,XX +XXX,XX @@ from django.views import View from django.http import HttpResponse, StreamingHttpResponse from django.utils.safestring import mark_safe -class ANSI2HTMLConverter(object): +class ANSIProcessor(object): RE_STRING = '[^\b\t\n\f\r\x1B]+' RE_NUMS = '[0-9]+(?:;[0-9]+)*' RE_CSI = r'\[\??(?:' + RE_NUMS + ')?[^;0-9]' RE_OSC = r'].*?(?:\x1B\\|\x07)' RE_CONTROL = '\x1B(?:%s|%s|[^][])|\r\n|[\b\t\n\f\r]' % (RE_CSI, RE_OSC) RE = re.compile('(%s)|(%s)' % (RE_STRING, RE_CONTROL)) - COLORS = [ "BLK", "RED", "GRN", "YEL", "BLU", "MAG", "CYN", "WHI", - "HIK", "HIR", "HIG", "HIY", "HIB", "HIM", "HIC", "HIW" ] - - ENTITIES = { - '\x00' : '␀', '\x01' : '␁', '\x02' : '␂', - '\x03' : '␃', '\x04' : '␄', '\x05' : '␅', - '\x06' : '␆', '\x07' : '🔔', '\x0B' : '␋', - '\x0E' : '␎', '\x0F' : '␏', '\x10' : '␐', - '\x11' : '␑', '\x12' : '␒', '\x13' : '␓', - '\x14' : '␔', '\x15' : '␕', '\x16' : '␖', - '\x17' : '␗', '\x18' : '␘', '\x19' : '␙', - '\x1A' : '␚', '\x1B' : '␛', '\x1C' : '␜', - '\x1D' : '␝', '\x1E' : '␞', '\x1F' : '␟', - '<' : '<', '>' : '>', '&' : '&', - '\x7F' : '⌦' - } - RE_ENTITIES = re.compile('[\x00-\x1F<>&\x7F]') - def __init__(self, white_bg=False): + def __init__(self): self.class_to_id = {} self.id_to_class = [] - self.default_fg = 0 if white_bg else 7 - self.default_bg = 7 if white_bg else 0 self.cur_class = self._class_to_id("") - self.prefix = '<pre class="ansi">' self._reset() self._reset_attrs() @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLConverter(object): self.line += [' '] * num self.class_ids += [0] * num - def _write_prefix(self): - if self.prefix != '': - yield self.prefix - self.prefix = '' - + @abc.abstractmethod def _write_span(self, text, class_id): - if class_id > 0: - yield ('<span class="%s">' % self.id_to_class[class_id]) - yield self.RE_ENTITIES.sub(lambda x: self.ENTITIES[x.group(0)], text) - if class_id > 0: - yield '</span>' + pass # Flushing a line locates spans that have the same style, and prints those # with a <span> tag if they are styled. @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLConverter(object): self.bg = arg - 92 def _write_form_feed(self): - yield '<hr>' + pass def _class_to_id(self, html_class): class_id = self.class_to_id.get(html_class, None) @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLConverter(object): self.id_to_class.append(html_class) return class_id - def _map_color(self, color, default, dim): - # map a color assigned by _do_one_csi_m to an index in the COLORS array - - color = color if color is not None else default - if dim: - # must be foreground color - if isinstance(color, int): - # unlike vte which has a "very dark" grey, for simplicity - # dark grey remains dark grey - return 8 if color == default or color == 8 else color&~8 - else: - return ('d' + color[0], 'd' + color[1]) - else: - if isinstance(color, int): - # use light colors by default, except for black and light grey - # (but see bold case in _compute_class) - return color if color == 0 or color == 7 else color|8 - else: - return color - def _compute_class(self): - fg = self._map_color(self.fg, self.default_fg, self.dim) - bg = self._map_color(self.bg, self.default_bg, False) - - # apply inverse now: "inverse dim" affects the *background* color! - if self.inverse: - fg, bg = bg, fg - - # bold turns foreground light grey into white - if fg == 7 and not self.dim and self.bold: - fg = 15 - - # now compute CSS classes - classes = [] - if isinstance(fg, int): - if fg != self.default_fg: - classes.append(self.COLORS[fg]) - else: - # 256-color palette - classes.append(fg[0]) - - if isinstance(bg, int): - if bg != self.default_bg: - classes.append('B' + self.COLORS[bg]) - else: - classes.append(bg[1]) - - if self.bold: - classes.append('BOLD') - if self.italic: - classes.append('ITA') - - if self.underline or self.strike: - undstr = '' - if self.underline: - undstr += 'UND' - if self.strike: - undstr += 'STR' - classes.append(undstr) - - self.cur_class = self._class_to_id(" ".join(classes)) + pass def _do_csi_m(self, it): try: @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLConverter(object): self._parse_csi_with_args(csi, self._do_csi_m) def convert(self, input): - yield from self._write_prefix() for m in self.RE.finditer(input): if m.group(1): if self.lazy_accumulate: @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLConverter(object): elif len(seq) > 1: yield from self._parse_csi(seq) + def finish(self): + yield from self._write_line('') + self._reset_attrs() + + +class ANSI2HTMLConverter(ANSIProcessor): + ENTITIES = { + '\x00' : '␀', '\x01' : '␁', '\x02' : '␂', + '\x03' : '␃', '\x04' : '␄', '\x05' : '␅', + '\x06' : '␆', '\x07' : '🔔', '\x0B' : '␋', + '\x0E' : '␎', '\x0F' : '␏', '\x10' : '␐', + '\x11' : '␑', '\x12' : '␒', '\x13' : '␓', + '\x14' : '␔', '\x15' : '␕', '\x16' : '␖', + '\x17' : '␗', '\x18' : '␘', '\x19' : '␙', + '\x1A' : '␚', '\x1B' : '␛', '\x1C' : '␜', + '\x1D' : '␝', '\x1E' : '␞', '\x1F' : '␟', + '<' : '<', '>' : '>', '&' : '&', + '\x7F' : '⌦' + } + RE_ENTITIES = re.compile('[\x00-\x1F<>&\x7F]') + + COLORS = [ "BLK", "RED", "GRN", "YEL", "BLU", "MAG", "CYN", "WHI", + "HIK", "HIR", "HIG", "HIY", "HIB", "HIM", "HIC", "HIW" ] + + def __init__(self, white_bg=False): + super(ANSI2HTMLConverter, self).__init__() + self.default_fg = 0 if white_bg else 7 + self.default_bg = 7 if white_bg else 0 + self.prefix = '<pre class="ansi">' + + def _write_prefix(self): + if self.prefix != '': + yield self.prefix + self.prefix = '' + + def _map_color(self, color, default, dim): + # map a color assigned by _do_one_csi_m to an index in the COLORS array + + color = color if color is not None else default + if dim: + # must be foreground color + if isinstance(color, int): + # unlike vte which has a "very dark" grey, for simplicity + # dark grey remains dark grey + return 8 if color == default or color == 8 else color&~8 + else: + return ('d' + color[0], 'd' + color[1]) + else: + if isinstance(color, int): + # use light colors by default, except for black and light grey + # (but see bold case in _compute_class) + return color if color == 0 or color == 7 else color|8 + else: + return color + + def _compute_class(self): + fg = self._map_color(self.fg, self.default_fg, self.dim) + bg = self._map_color(self.bg, self.default_bg, False) + + # apply inverse now: "inverse dim" affects the *background* color! + if self.inverse: + fg, bg = bg, fg + + # bold turns foreground light grey into white + if fg == 7 and not self.dim and self.bold: + fg = 15 + + # now compute CSS classes + classes = [] + if isinstance(fg, int): + if fg != self.default_fg: + classes.append(self.COLORS[fg]) + else: + # 256-color palette + classes.append(fg[0]) + + if isinstance(bg, int): + if bg != self.default_bg: + classes.append('B' + self.COLORS[bg]) + else: + classes.append(bg[1]) + + if self.bold: + classes.append('BOLD') + if self.italic: + classes.append('ITA') + + if self.underline or self.strike: + undstr = '' + if self.underline: + undstr += 'UND' + if self.strike: + undstr += 'STR' + classes.append(undstr) + + self.cur_class = self._class_to_id(" ".join(classes)) + + def _write_span(self, text, class_id): + if class_id > 0: + yield ('<span class="%s">' % self.id_to_class[class_id]) + yield self.RE_ENTITIES.sub(lambda x: self.ENTITIES[x.group(0)], text) + if class_id > 0: + yield '</span>' + + def _write_form_feed(self): + yield '<hr>' + + def convert(self, input): + yield from self._write_prefix() + yield from super(ANSI2HTMLConverter, self).convert(input) + def finish(self): yield from self._write_prefix() - yield from self._write_line('</pre>') + yield from super(ANSI2HTMLConverter, self).finish() + yield '</pre>' self.prefix = '<pre class="ansi">' - self._reset_attrs() + def ansi2html(input, white_bg=False): c = ANSI2HTMLConverter(white_bg=white_bg) -- 2.14.3
This converter compresses sequences involving carriage returns or backspaces, to make them readable from an editor or mail reader. Signed-off-by: Paolo Bonzini <pbonzini@redhat.com> --- patchew/logviewer.py | 29 ++++++++++++++++++ patchew/settings.py | 3 ++ patchew/tags.py | 19 ++++++++++++ tests/test_ansi2html.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++- tests/test_custom_tags.py | 27 +++++++++++++++++ 5 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 patchew/tags.py create mode 100755 tests/test_custom_tags.py diff --git a/patchew/logviewer.py b/patchew/logviewer.py index XXXXXXX..XXXXXXX 100644 --- a/patchew/logviewer.py +++ b/patchew/logviewer.py @@ -XXX,XX +XXX,XX @@ class ANSIProcessor(object): self._reset_attrs() +class ANSI2TextConverter(ANSIProcessor): + FF = '\u2015' * 72 + '\n' + SYMBOLS = { + '\x00' : '\u2400', '\x01' : '\u2401', '\x02' : '\u2402', + '\x03' : '\u2403', '\x04' : '\u2404', '\x05' : '\u2405', + '\x06' : '\u2406', '\x07' : '\U00001F514', '\x0B' : '\u240B', + '\x0E' : '\u240E', '\x0F' : '\u240F', '\x10' : '\u2410', + '\x11' : '\u2411', '\x12' : '\u2412', '\x13' : '\u2413', + '\x14' : '\u2414', '\x15' : '\u2415', '\x16' : '\u2416', + '\x17' : '\u2417', '\x18' : '\u2418', '\x19' : '\u2419', + '\x1A' : '\u241A', '\x1B' : '\u241B', '\x1C' : '\u241C', + '\x1D' : '\u241D', '\x1E' : '\u241E', '\x1F' : '\u241F', + '\x7F' : '\u2326' + } + RE_SYMBOLS = re.compile('[\x00-\x1F\x7F]') + + def _write_span(self, text, class_id): + yield self.RE_SYMBOLS.sub(lambda x: self.SYMBOLS[x.group(0)], text) + + def _write_form_feed(self): + yield self.FF + + class ANSI2HTMLConverter(ANSIProcessor): ENTITIES = { '\x00' : '␀', '\x01' : '␁', '\x02' : '␂', @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLConverter(ANSIProcessor): self.prefix = '<pre class="ansi">' +def ansi2text(input): + c = ANSI2TextConverter() + yield from c.convert(input) + yield from c.finish() + + def ansi2html(input, white_bg=False): c = ANSI2HTMLConverter(white_bg=white_bg) yield from c.convert(input) diff --git a/patchew/settings.py b/patchew/settings.py index XXXXXXX..XXXXXXX 100644 --- a/patchew/settings.py +++ b/patchew/settings.py @@ -XXX,XX +XXX,XX @@ TEMPLATES = [ 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], + 'builtins': [ + 'patchew.tags', + ], }, }, ] diff --git a/patchew/tags.py b/patchew/tags.py new file mode 100644 index XXXXXXX..XXXXXXX --- /dev/null +++ b/patchew/tags.py @@ -XXX,XX +XXX,XX @@ +#!/usr/bin/env python3 +# +# Copyright 2018 Red Hat, Inc. +# +# Authors: +# Paolo Bonzini <pbonzini@redhat.com> +# +# This work is licensed under the MIT License. Please see the LICENSE file or +# http://opensource.org/licenses/MIT. + +from django import template +from patchew import logviewer + +register = template.Library() + +@register.simple_tag +@register.filter +def ansi2text(value): + return ''.join(logviewer.ansi2text(value)) diff --git a/tests/test_ansi2html.py b/tests/test_ansi2html.py index XXXXXXX..XXXXXXX 100644 --- a/tests/test_ansi2html.py +++ b/tests/test_ansi2html.py @@ -XXX,XX +XXX,XX @@ import unittest -from patchew.logviewer import ansi2html +from patchew.logviewer import ansi2html, ansi2text, ANSI2TextConverter class ANSI2HTMLTest(unittest.TestCase): def assertAnsi(self, test, expected, **kwargs): @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLTest(unittest.TestCase): self.assertWhiteBg('abc\x1b[7m\x1b[1Kabc', ' <span class="WHI BBLK">abc</span>') +class ANSI2TextTest(unittest.TestCase): + def assertAnsi(self, test, expected, **kwargs): + self.assertEqual(''.join(ansi2text(test, **kwargs)), expected, + repr(test)) + + # basic formatting tests + def test_basic(self): + self.assertAnsi('\tb', ' b') + self.assertAnsi('\t\ta', ' a') + self.assertAnsi('a\tb', 'a b') + self.assertAnsi('ab\tc', 'ab c') + self.assertAnsi('a\nbc', 'a\nbc') + self.assertAnsi('a\f', 'a\n' + ANSI2TextConverter.FF) + self.assertAnsi('a\n\f', 'a\n\n' + ANSI2TextConverter.FF) + self.assertAnsi('<', '<') + self.assertAnsi('\x07', '\U00001F514') + + # backspace and carriage return + def test_set_pos(self): + self.assertAnsi('abc\b\bBC', 'aBC') + self.assertAnsi('a\b<', '<') + self.assertAnsi('<\ba', 'a') + self.assertAnsi('a\b\bbc', 'bc') + self.assertAnsi('a\rbc', 'bc') + self.assertAnsi('a\nb\bc', 'a\nc') + self.assertAnsi('a\t\bb', 'a b') + self.assertAnsi('a\tb\b\bc', 'a cb') + self.assertAnsi('01234567\r\tb', '01234567b') + + # Escape sequences + def test_esc_parsing(self): + self.assertAnsi('{\x1b%}', '{}') + self.assertAnsi('{\x1b[0m}', '{}') + self.assertAnsi('{\x1b[m}', '{}') + self.assertAnsi('{\x1b[0;1;7;0m}', '{}') + self.assertAnsi('{\x1b[1;7m\x1b[m}', '{}') + self.assertAnsi('{\x1b]test\x1b\\}', '{}') + self.assertAnsi('{\x1b]test\x07}', '{}') + self.assertAnsi('{\x1b]test\x1b[0m\x07}', '{}') + self.assertAnsi('{\x1b]test\x1b[7m\x07}', '{}') + + # ESC [C and ESC [D + def test_horiz_movement(self): + self.assertAnsi('abc\x1b[2DB', 'aBc') + self.assertAnsi('abc\x1b[3CD', 'abc D') + self.assertAnsi('abcd\x1b[3DB\x1b[1CD', 'aBcD') + self.assertAnsi('abc\x1b[0CD', 'abc D') + self.assertAnsi('abc\x1b[CD', 'abc D') + + # ESC [K + def test_clear_line(self): + self.assertAnsi('\x1b[Kabcd', 'abcd') + self.assertAnsi('abcd\r\x1b[K', '') + self.assertAnsi('abcd\b\x1b[K', 'abc') + self.assertAnsi('abcd\r\x1b[KDef', 'Def') + self.assertAnsi('abcd\b\x1b[KDef', 'abcDef') + self.assertAnsi('abcd\r\x1b[0K', '') + self.assertAnsi('abcd\b\x1b[0K', 'abc') + self.assertAnsi('abcd\r\x1b[1K', 'abcd') + self.assertAnsi('abcd\b\x1b[1K', ' d') + self.assertAnsi('abcd\r\x1b[2K', '') + self.assertAnsi('abcd\b\x1b[2K', ' ') + self.assertAnsi('abcd\r\x1b[2KDef', 'Def') + self.assertAnsi('abcd\b\x1b[2KDef', ' Def') + + # combining cursor movement and formatting + def test_movement_and_formatting(self): + self.assertAnsi('\x1b[42m\tabc', ' abc') + self.assertAnsi('abc\x1b[42m\x1b[1Kabc', ' abc') + self.assertAnsi('\x1b[7m\tabc', ' abc') + self.assertAnsi('abc\x1b[7m\x1b[1Kabc', ' abc') + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_custom_tags.py b/tests/test_custom_tags.py new file mode 100755 index XXXXXXX..XXXXXXX --- /dev/null +++ b/tests/test_custom_tags.py @@ -XXX,XX +XXX,XX @@ +#!/usr/bin/env python3 +# +# Copyright 2018 Red Hat, Inc. +# +# Authors: +# Paolo Bonzini <pbonzini@redhat.com> +# +# This work is licensed under the MIT License. Please see the LICENSE file or +# http://opensource.org/licenses/MIT. + +from django.template import Context, Template +import patchewtest +import unittest + +class CustomTagsTest(unittest.TestCase): + def assertTemplate(self, template, expected, **kwargs): + context = Context(kwargs) + self.assertEqual(Template(template).render(context), expected) + + def test_template_filters(self): + self.assertTemplate('{{s|ansi2text}}', 'dbc', s='abc\rd') + + def test_template_tags(self): + self.assertTemplate('{% ansi2text s %}', 'dbc', s='abc\rd') + +if __name__ == '__main__': + unittest.main() -- 2.14.3
These can be used to make test report emails shorter and more readable. Signed-off-by: Paolo Bonzini <pbonzini@redhat.com> --- patchew/tags.py | 77 ++++++++++++++++++++++++++++++++++ tests/test_custom_tags.py | 102 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+) diff --git a/patchew/tags.py b/patchew/tags.py index XXXXXXX..XXXXXXX 100644 --- a/patchew/tags.py +++ b/patchew/tags.py @@ -XXX,XX +XXX,XX @@ from django import template from patchew import logviewer +from collections import deque +import io +import operator +import re + +# The basic implementation uses generators. The filters simply apply join +# to the result of the generators. + +def lines_iter(value): + if not isinstance(value, io.IOBase): + # StringIO provides a generator to split lines. + value = io.StringIO(value) + # "chomp" the newlines on each line. Using operator and map does + # everything in the interpreter, avoiding the overhead of a generator. + return map(operator.methodcaller('rstrip', '\r\n'), value) + +# To understand grep_iter, it may help to first study this implementation +# of a "tail" iterator, which is based on the same circular array idea: +# +# def tail_lines_iter(value, n): +# lines = [None] * n +# lineno = 0 +# for line in lines_iter(value): +# lines[lineno % n] = line +# lineno += 1 +# +# for i in range(max(lineno - n, 0), lineno): +# yield lines[i % n] +# +# Basic "grep" prints one line when the match is on the last line, so +# "grep" is a variation on tail_lines with n=1; likewise, "grep -B1" is +# a variantion on tail_lines with n=2, etc. + +def grep_iter(value, regex, n_before, n_after, sep): + n = n_before + 1 + lines = [None] * n + stop = lineno = 0 + for line in lines_iter(value): + # Print the (lineno - n)-th line. Each element of lines[] is used + # just before it is thrown away. + if lineno - n >= 0 and lineno - n < stop: + yield lines[lineno % n] + if re.search(regex, line): + if lineno - n >= stop and sep is not None and stop > 0: + yield sep + stop = lineno + n_after + 1 + lines[lineno % n] = line + lineno += 1 + + for i in range(max(lineno - n, 0), min(stop, lineno)): + yield lines[i % n] register = template.Library() @@ -XXX,XX +XXX,XX @@ register = template.Library() @register.filter def ansi2text(value): return ''.join(logviewer.ansi2text(value)) + +@register.simple_tag +@register.filter +def tail_lines(value, n): + lines = deque(lines_iter(value), n) + return '\n'.join(lines) + +@register.simple_tag +@register.filter +def grep(value, regex, sep=None): + return '\n'.join(grep_iter(value, regex, 0, 0, sep)) + +@register.simple_tag +@register.filter +def grep_A(value, regex, n=3, sep='---'): + return '\n'.join(grep_iter(value, regex, 0, n, sep)) + +@register.simple_tag +@register.filter +def grep_B(value, regex, n=3, sep='---'): + return '\n'.join(grep_iter(value, regex, n, 0, sep)) + +@register.simple_tag +@register.filter +def grep_C(value, regex, n=3, sep='---'): + return '\n'.join(grep_iter(value, regex, n, n, sep)) diff --git a/tests/test_custom_tags.py b/tests/test_custom_tags.py index XXXXXXX..XXXXXXX 100755 --- a/tests/test_custom_tags.py +++ b/tests/test_custom_tags.py @@ -XXX,XX +XXX,XX @@ # http://opensource.org/licenses/MIT. from django.template import Context, Template +from patchew.tags import tail_lines, grep_A, grep_B, grep_C, grep import patchewtest import unittest @@ -XXX,XX +XXX,XX @@ class CustomTagsTest(unittest.TestCase): def test_template_filters(self): self.assertTemplate('{{s|ansi2text}}', 'dbc', s='abc\rd') + self.assertTemplate('{{s|grep:"[0-9]"}}', '0\n9', s='0\na\n9') + self.assertTemplate('{{s|grep_A:"b"}}', + 'b\nc\nd\ne\n---\nb', + s='a\nb\nc\nd\ne\nf\nx\ny\nz\nb') + self.assertTemplate('{{s|grep_B:"b"}}', + 'a\nb\n---\nx\ny\nz\nb', + s='a\nb\nc\nd\ne\nf\nx\ny\nz\nb') + self.assertTemplate('{{s|grep_C:"b"}}', + 'a\nb\nc\nd\ne\n---\nx\ny\nz\nb', + s='a\nb\nc\nd\ne\nf\nx\ny\nz\nb') + self.assertTemplate('{{s|tail_lines:3}}', 'b\nc\nd', s='a\nb\nc\nd') def test_template_tags(self): self.assertTemplate('{% ansi2text s %}', 'dbc', s='abc\rd') + self.assertTemplate('{% grep s "[0-9]" %}', '0\n9', s='0\na\n9') + self.assertTemplate('{% grep_A s regex="[bc]" n=1 %}', 'b\nc\nd', s='a\nb\nc\nd') + self.assertTemplate('{% grep_B s regex="[bc]" n=1 %}', 'a\nb\nc', s='a\nb\nc\nd') + self.assertTemplate('{% grep_C s "b" n=1 %}', 'a\nb\nc', s='a\nb\nc\nd') + self.assertTemplate('{% tail_lines s n=3 %}', 'b\nc\nd', s='a\nb\nc\nd') + + def test_grep(self): + self.assertEqual(grep('0\na\n9', '[0-9]'), '0\n9') + self.assertEqual(grep('0\na\n9', '[0-9]', '---'), '0\n---\n9') + + def test_grep_A(self): + self.assertEqual(grep_A('a\nb\nc\nd', 'b', 1, None), 'b\nc') + self.assertEqual(grep_A('a\nb\nc\nd', 'b', 2, None), 'b\nc\nd') + self.assertEqual(grep_A('a\nb\nc\nd\nb\ne', 'b', 1, None), 'b\nc\nb\ne') + self.assertEqual(grep_A('a\nb\nc\nd\nb\ne', 'b', 2, None), 'b\nc\nd\nb\ne') + self.assertEqual(grep_A('a\nb\nc\nd\nz\nb\ne', 'b', 1, None), 'b\nc\nb\ne') + self.assertEqual(grep_A('a\nb\nc\nd\nz\nb\ne', 'b', 2, None), 'b\nc\nd\nb\ne') + self.assertEqual(grep_A('a\nb\nc\nz\nb\nb\ne', 'b', 1, None), 'b\nc\nb\nb\ne') + self.assertEqual(grep_A('b\nc\nz\nb\nb\ne', 'b', 1, None), 'b\nc\nb\nb\ne') + self.assertEqual(grep_A('b\n', 'b', 1, None), 'b') + + def test_grep_A_sep(self): + self.assertEqual(grep_A('a\nb\nc\nd', 'b', 1), 'b\nc') + self.assertEqual(grep_A('a\nb\nc\nd', 'b', 2), 'b\nc\nd') + self.assertEqual(grep_A('a\nb\nc\nd\nb\ne', 'b', 1), 'b\nc\n---\nb\ne') + self.assertEqual(grep_A('a\nb\nc\nd\nb\ne', 'b', 2), 'b\nc\nd\nb\ne') + self.assertEqual(grep_A('a\nb\nc\nd\nz\nb\ne', 'b', 1), 'b\nc\n---\nb\ne') + self.assertEqual(grep_A('a\nb\nc\nd\nz\nb\nb\ne', 'b', 1), 'b\nc\n---\nb\nb\ne') + self.assertEqual(grep_A('b\nc\nz\nb\nb\ne', 'b', 1), 'b\nc\n---\nb\nb\ne') + self.assertEqual(grep_A('b\n', 'b', 1), 'b') + + def test_grep_B(self): + self.assertEqual(grep_B('a\nb\nc\nd', 'b', 1, None), 'a\nb') + self.assertEqual(grep_B('a\nb\nc\nd', 'b', 2, None), 'a\nb') + self.assertEqual(grep_B('a\nb\nc\nd\nb\ne', 'b', 1, None), 'a\nb\nd\nb') + self.assertEqual(grep_B('a\nb\nc\nd\nz\nb\ne', 'b', 1, None), 'a\nb\nz\nb') + self.assertEqual(grep_B('a\nb\nc\nd\nz\nb\ne', 'b', 2, None), 'a\nb\nd\nz\nb') + self.assertEqual(grep_B('a\nb\nc\nz\nb\nb\ne', 'b', 1, None), 'a\nb\nz\nb\nb') + self.assertEqual(grep_B('b\nc\nz\nb\nb\ne', 'b', 1, None), 'b\nz\nb\nb') + self.assertEqual(grep_B('b\n', 'b', 1, None), 'b') + + def test_grep_B_sep(self): + self.assertEqual(grep_B('a\nb\nc\nd', 'b', 1), 'a\nb') + self.assertEqual(grep_B('a\nb\nc\nd', 'b', 2), 'a\nb') + self.assertEqual(grep_B('a\nb\nc\nd\nb\ne', 'b', 1), 'a\nb\n---\nd\nb') + self.assertEqual(grep_B('a\nb\nc\nd\nz\nb\ne', 'b', 1), 'a\nb\n---\nz\nb') + self.assertEqual(grep_B('a\nb\nc\nd\nz\nb\ne', 'b', 2), 'a\nb\n---\nd\nz\nb') + self.assertEqual(grep_B('a\nb\nc\nz\nb\nb\ne', 'b', 1), 'a\nb\n---\nz\nb\nb') + self.assertEqual(grep_B('b\nc\nz\nb\nb\ne', 'b', 1), 'b\n---\nz\nb\nb') + self.assertEqual(grep_B('b\n', 'b', 1), 'b') + + def test_grep_C(self): + self.assertEqual(grep_C('a\nb\nc\nd', 'b', 1, None), 'a\nb\nc') + self.assertEqual(grep_C('a\nb\nc\nd', 'b', 2, None), 'a\nb\nc\nd') + self.assertEqual(grep_C('a\nb\nc\nd\nb\ne', 'b', 1, None), 'a\nb\nc\nd\nb\ne') + self.assertEqual(grep_C('a\nb\nc\nd\nz\nb\ne', 'b', 1, None), 'a\nb\nc\nz\nb\ne') + self.assertEqual(grep_C('a\nb\nc\nd\nz\nb\ne', 'b', 2, None), 'a\nb\nc\nd\nz\nb\ne') + self.assertEqual(grep_C('a\nb\nc\nz\nb\nb\ne', 'b', 1, None), 'a\nb\nc\nz\nb\nb\ne') + self.assertEqual(grep_C('b\nc\nz\nb\nb\ne', 'b', 1, None), 'b\nc\nz\nb\nb\ne') + self.assertEqual(grep_C('b\n', 'b', 1, None), 'b') + + def test_grep_C_sep(self): + self.assertEqual(grep_C('a\nb\nc\nd', 'b', 1), 'a\nb\nc') + self.assertEqual(grep_C('a\nb\nc\nd', 'b', 2), 'a\nb\nc\nd') + self.assertEqual(grep_C('a\nb\nc\nd\nb\ne', 'b', 1), 'a\nb\nc\nd\nb\ne') + self.assertEqual(grep_C('a\nb\nc\nd\nz\nb\ne', 'b', 1), 'a\nb\nc\n---\nz\nb\ne') + self.assertEqual(grep_C('a\nb\nc\nd\nz\nb\ne', 'b', 2), 'a\nb\nc\nd\nz\nb\ne') + self.assertEqual(grep_C('a\nb\nc\nz\nb\nb\ne', 'b', 1), 'a\nb\nc\nz\nb\nb\ne') + self.assertEqual(grep_C('b\nc\nz\nb\nb\ne', 'b', 1), 'b\nc\nz\nb\nb\ne') + self.assertEqual(grep_C('b\n', 'b', 1), 'b') + + def test_tail_lines(self): + self.assertEqual(tail_lines('', 0), '') + self.assertEqual(tail_lines('', 1), '') + self.assertEqual(tail_lines('', 2), '') + self.assertEqual(tail_lines('', 4), '') + + self.assertEqual(tail_lines('a\nb\n', 0), '') + self.assertEqual(tail_lines('a\nb\n', 1), 'b') + self.assertEqual(tail_lines('a\nb\n', 2), 'a\nb') + + self.assertEqual(tail_lines('a\nb\nc\n', 2), 'b\nc') + self.assertEqual(tail_lines('a\nb\nc\n', 4), 'a\nb\nc') + + self.assertEqual(tail_lines('a\nb\nc\nd\n', 2), 'c\nd') + + self.assertEqual(tail_lines('\n\n\n', 2), '\n') + self.assertEqual(tail_lines('\n\n\nbc', 2), '\nbc') + self.assertEqual(tail_lines('\n\nbc', 3), '\n\nbc') + self.assertEqual(tail_lines('\n\n\n\nbc', 3), '\n\nbc') if __name__ == '__main__': unittest.main() -- 2.14.3
Abstract the creation of testing log URLs. Signed-off-by: Paolo Bonzini <pbonzini@redhat.com> --- mods/testing.py | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/mods/testing.py b/mods/testing.py index XXXXXXX..XXXXXXX 100644 --- a/mods/testing.py +++ b/mods/testing.py @@ -XXX,XX +XXX,XX @@ class TestingModule(PatchewModule): TestingLogViewer.as_view(), name="testing-log")) + def reverse_testing_log(self, obj, test, request=None, html=False): + if isinstance(obj, Message): + log_url = reverse("testing-log", + kwargs={"project_or_series": obj.message_id, + "testing_name": test}) + "?type=message" + else: + assert(isinstance(obj, Project)) + log_url = reverse.reverse("testing-log", + kwargs={"project_or_series": obj.name, + "testing_name": test}) + "?type=project" + if html: + log_url += "&html=1" + # Generate a full URL, including the host and port, for use in email + # notifications and REST API responses. + if request: + log_url = request.build_absolute_uri(url) + return log_url + def add_test_report(self, user, project, tester, test, head, base, identity, passed, log, is_timeout): # Find a project or series depending on the test type and assign it to obj @@ -XXX,XX +XXX,XX @@ class TestingModule(PatchewModule): continue tn = pn[len("testing.report."):] failed = not p["passed"] - log_prop = "testing.log." + tn - if isinstance(obj, Message): - typearg = "type=message" - log_url = reverse("testing-log", - kwargs={"project_or_series": obj.message_id, - "testing_name": tn}) - else: - assert(isinstance(obj, Project)) - typearg = "type=project" - log_url = reverse("testing-log", - kwargs={"project_or_series": obj.name, - "testing_name": tn}) - log_url += "?" + typearg - html_log_url = log_url + "&html=1" + log_url = self.reverse_testing_log(obj, tn, html=False) + html_log_url = self.reverse_testing_log(obj, tn, html=True) passed_str = "failed" if failed else "passed" html = format_html('Test <b>{}</b> <a class="cbox-log" data-link="{}" href="{}">{}</a>', tn, html_log_url, log_url, passed_str) @@ -XXX,XX +XXX,XX @@ class TestingModule(PatchewModule): continue tn = pn[len("testing.report."):] failed = not p["passed"] - log_prop = "testing.log." + tn - typearg = "type=message" - log_url = reverse("testing-log", - kwargs={"project_or_series": message.message_id, - "testing_name": tn}) - log_url += "?" + typearg + log_url = self.reverse_testing_log(message, tn, request=request, html=False) passed_str = "failure" if failed else "success" result = { 'status': passed_str, -- 2.14.3
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com> --- mods/testing.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/mods/testing.py b/mods/testing.py index XXXXXXX..XXXXXXX 100644 --- a/mods/testing.py +++ b/mods/testing.py @@ -XXX,XX +XXX,XX @@ class TestingModule(PatchewModule): log_url = request.build_absolute_uri(url) return log_url - def add_test_report(self, user, project, tester, test, head, + def add_test_report(self, request, project, tester, test, head, base, identity, passed, log, is_timeout): # Find a project or series depending on the test type and assign it to obj if identity["type"] == "project": obj = Project.objects.get(name=project) - is_proj_report = True project = obj.name elif identity["type"] == "series": message_id = identity["message-id"] obj = Message.objects.find_series(message_id, project) if not obj: raise Exception("Series doesn't exist") - is_proj_report = False project = obj.project.name + user = request.user + log_url = self.reverse_testing_log(obj, test, request=request) + html_log_url = self.reverse_testing_log(obj, test, request=request, html=True) obj.set_property("testing.report." + test, {"passed": passed, "is_timeout": is_timeout, @@ -XXX,XX +XXX,XX @@ class TestingModule(PatchewModule): if all_tests.issubset(done_tests): obj.set_property("testing.tested-head", head) emit_event("TestingReport", tester=tester, user=user.username, - obj=obj, passed=passed, test=test, log=log, - is_timeout=is_timeout) + obj=obj, passed=passed, test=test, log=log, log_url=log_url, + html_log_url=html_log_url, is_timeout=is_timeout) def get_tests(self, obj): ret = {} @@ -XXX,XX +XXX,XX @@ class TestingReportView(APILoginRequiredView): head, base, passed, log, identity, is_timeout=False): _instance.tester_check_in(project, tester or request.user.username) - _instance.add_test_report(request.user, project, tester, + _instance.add_test_report(request, project, tester, test, head, base, identity, passed, log, is_timeout) -- 2.14.3
These patches add template tags and filters designed for use in email sent by Patchew: convert ANSI to text, and grep with context. Once committed, I'd like to try this out by setting up an email hook on next.patchew.org. Can you send me the template you're using for QEMU notifications on the master site? Paolo RFC->v1: - add patch 5 - fix tests - use U+2500 BOX DRAWINGS LIGHT HORIZONTAL for \f Paolo Bonzini (7): ansi2html: add _write_form_feed ansi2html: create ANSIProcessor superclass ansi2html: create ANSI to text converter add custom template tags for manipulating testing logs rest: pass request to hooks testing: add reverse_testing_log testing: pass log URL in the TestingReport event api/rest.py | 12 ++- mods/diff.py | 2 +- mods/git.py | 2 +- mods/tags.py | 2 +- mods/testing.py | 56 ++++++----- patchew/logviewer.py | 252 ++++++++++++++++++++++++++++------------------ patchew/settings.py | 3 + patchew/tags.py | 96 ++++++++++++++++++ tests/test_ansi2html.py | 75 +++++++++++++- tests/test_custom_tags.py | 129 ++++++++++++++++++++++++ 10 files changed, 498 insertions(+), 131 deletions(-) create mode 100644 patchew/tags.py create mode 100755 tests/test_custom_tags.py -- 2.14.3 _______________________________________________ Patchew-devel mailing list Patchew-devel@redhat.com https://www.redhat.com/mailman/listinfo/patchew-devel
Form feeds are rendered differently by HTML and text converters, so move the HTML rendering out of _write_line's argument. Signed-off-by: Paolo Bonzini <pbonzini@redhat.com> --- patchew/logviewer.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/patchew/logviewer.py b/patchew/logviewer.py index XXXXXXX..XXXXXXX 100644 --- a/patchew/logviewer.py +++ b/patchew/logviewer.py @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLConverter(object): # the remaining light colors: dark grey and white self.bg = arg - 92 + def _write_form_feed(self): + yield '<hr>' + def _class_to_id(self, html_class): class_id = self.class_to_id.get(html_class, None) if class_id is None: @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLConverter(object): if csi[-1] == 'J': save_pos = self.pos - yield from self._write_line('<hr>') + yield from self._write_line('') + yield from self._write_form_feed() self._set_pos(save_pos) elif csi[-1] == 'K': self._parse_csi_with_args(csi, self._do_csi_K) @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLConverter(object): yield from self._write_line('\n') continue elif seq == '\f': - yield from self._write_line('\n<hr>') + yield from self._write_line('\n') + yield from self._write_form_feed() continue if self.lazy_contents != '': -- 2.14.3 _______________________________________________ Patchew-devel mailing list Patchew-devel@redhat.com https://www.redhat.com/mailman/listinfo/patchew-devel
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com> --- patchew/logviewer.py | 216 ++++++++++++++++++++++++++++----------------------- 1 file changed, 120 insertions(+), 96 deletions(-) diff --git a/patchew/logviewer.py b/patchew/logviewer.py index XXXXXXX..XXXXXXX 100644 --- a/patchew/logviewer.py +++ b/patchew/logviewer.py @@ -XXX,XX +XXX,XX @@ from django.views import View from django.http import HttpResponse, StreamingHttpResponse from django.utils.safestring import mark_safe -class ANSI2HTMLConverter(object): +class ANSIProcessor(object): RE_STRING = '[^\b\t\n\f\r\x1B]+' RE_NUMS = '[0-9]+(?:;[0-9]+)*' RE_CSI = r'\[\??(?:' + RE_NUMS + ')?[^;0-9]' RE_OSC = r'].*?(?:\x1B\\|\x07)' RE_CONTROL = '\x1B(?:%s|%s|[^][])|\r\n|[\b\t\n\f\r]' % (RE_CSI, RE_OSC) RE = re.compile('(%s)|(%s)' % (RE_STRING, RE_CONTROL)) - COLORS = [ "BLK", "RED", "GRN", "YEL", "BLU", "MAG", "CYN", "WHI", - "HIK", "HIR", "HIG", "HIY", "HIB", "HIM", "HIC", "HIW" ] - - ENTITIES = { - '\x00' : '␀', '\x01' : '␁', '\x02' : '␂', - '\x03' : '␃', '\x04' : '␄', '\x05' : '␅', - '\x06' : '␆', '\x07' : '🔔', '\x0B' : '␋', - '\x0E' : '␎', '\x0F' : '␏', '\x10' : '␐', - '\x11' : '␑', '\x12' : '␒', '\x13' : '␓', - '\x14' : '␔', '\x15' : '␕', '\x16' : '␖', - '\x17' : '␗', '\x18' : '␘', '\x19' : '␙', - '\x1A' : '␚', '\x1B' : '␛', '\x1C' : '␜', - '\x1D' : '␝', '\x1E' : '␞', '\x1F' : '␟', - '<' : '<', '>' : '>', '&' : '&', - '\x7F' : '⌦' - } - RE_ENTITIES = re.compile('[\x00-\x1F<>&\x7F]') - def __init__(self, white_bg=False): + def __init__(self): self.class_to_id = {} self.id_to_class = [] - self.default_fg = 0 if white_bg else 7 - self.default_bg = 7 if white_bg else 0 self.cur_class = self._class_to_id("") - self.prefix = '<pre class="ansi">' self._reset() self._reset_attrs() @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLConverter(object): self.line += [' '] * num self.class_ids += [0] * num - def _write_prefix(self): - if self.prefix != '': - yield self.prefix - self.prefix = '' - + @abc.abstractmethod def _write_span(self, text, class_id): - if class_id > 0: - yield ('<span class="%s">' % self.id_to_class[class_id]) - yield self.RE_ENTITIES.sub(lambda x: self.ENTITIES[x.group(0)], text) - if class_id > 0: - yield '</span>' + pass # Flushing a line locates spans that have the same style, and prints those # with a <span> tag if they are styled. @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLConverter(object): self.bg = arg - 92 def _write_form_feed(self): - yield '<hr>' + pass def _class_to_id(self, html_class): class_id = self.class_to_id.get(html_class, None) @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLConverter(object): self.id_to_class.append(html_class) return class_id - def _map_color(self, color, default, dim): - # map a color assigned by _do_one_csi_m to an index in the COLORS array - - color = color if color is not None else default - if dim: - # must be foreground color - if isinstance(color, int): - # unlike vte which has a "very dark" grey, for simplicity - # dark grey remains dark grey - return 8 if color == default or color == 8 else color&~8 - else: - return ('d' + color[0], 'd' + color[1]) - else: - if isinstance(color, int): - # use light colors by default, except for black and light grey - # (but see bold case in _compute_class) - return color if color == 0 or color == 7 else color|8 - else: - return color - def _compute_class(self): - fg = self._map_color(self.fg, self.default_fg, self.dim) - bg = self._map_color(self.bg, self.default_bg, False) - - # apply inverse now: "inverse dim" affects the *background* color! - if self.inverse: - fg, bg = bg, fg - - # bold turns foreground light grey into white - if fg == 7 and not self.dim and self.bold: - fg = 15 - - # now compute CSS classes - classes = [] - if isinstance(fg, int): - if fg != self.default_fg: - classes.append(self.COLORS[fg]) - else: - # 256-color palette - classes.append(fg[0]) - - if isinstance(bg, int): - if bg != self.default_bg: - classes.append('B' + self.COLORS[bg]) - else: - classes.append(bg[1]) - - if self.bold: - classes.append('BOLD') - if self.italic: - classes.append('ITA') - - if self.underline or self.strike: - undstr = '' - if self.underline: - undstr += 'UND' - if self.strike: - undstr += 'STR' - classes.append(undstr) - - self.cur_class = self._class_to_id(" ".join(classes)) + pass def _do_csi_m(self, it): try: @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLConverter(object): self._parse_csi_with_args(csi, self._do_csi_m) def convert(self, input): - yield from self._write_prefix() for m in self.RE.finditer(input): if m.group(1): if self.lazy_accumulate: @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLConverter(object): elif len(seq) > 1: yield from self._parse_csi(seq) + def finish(self): + yield from self._write_line('') + self._reset_attrs() + + +class ANSI2HTMLConverter(ANSIProcessor): + ENTITIES = { + '\x00' : '␀', '\x01' : '␁', '\x02' : '␂', + '\x03' : '␃', '\x04' : '␄', '\x05' : '␅', + '\x06' : '␆', '\x07' : '🔔', '\x0B' : '␋', + '\x0E' : '␎', '\x0F' : '␏', '\x10' : '␐', + '\x11' : '␑', '\x12' : '␒', '\x13' : '␓', + '\x14' : '␔', '\x15' : '␕', '\x16' : '␖', + '\x17' : '␗', '\x18' : '␘', '\x19' : '␙', + '\x1A' : '␚', '\x1B' : '␛', '\x1C' : '␜', + '\x1D' : '␝', '\x1E' : '␞', '\x1F' : '␟', + '<' : '<', '>' : '>', '&' : '&', + '\x7F' : '⌦' + } + RE_ENTITIES = re.compile('[\x00-\x1F<>&\x7F]') + + COLORS = [ "BLK", "RED", "GRN", "YEL", "BLU", "MAG", "CYN", "WHI", + "HIK", "HIR", "HIG", "HIY", "HIB", "HIM", "HIC", "HIW" ] + + def __init__(self, white_bg=False): + super(ANSI2HTMLConverter, self).__init__() + self.default_fg = 0 if white_bg else 7 + self.default_bg = 7 if white_bg else 0 + self.prefix = '<pre class="ansi">' + + def _write_prefix(self): + if self.prefix != '': + yield self.prefix + self.prefix = '' + + def _map_color(self, color, default, dim): + # map a color assigned by _do_one_csi_m to an index in the COLORS array + + color = color if color is not None else default + if dim: + # must be foreground color + if isinstance(color, int): + # unlike vte which has a "very dark" grey, for simplicity + # dark grey remains dark grey + return 8 if color == default or color == 8 else color&~8 + else: + return ('d' + color[0], 'd' + color[1]) + else: + if isinstance(color, int): + # use light colors by default, except for black and light grey + # (but see bold case in _compute_class) + return color if color == 0 or color == 7 else color|8 + else: + return color + + def _compute_class(self): + fg = self._map_color(self.fg, self.default_fg, self.dim) + bg = self._map_color(self.bg, self.default_bg, False) + + # apply inverse now: "inverse dim" affects the *background* color! + if self.inverse: + fg, bg = bg, fg + + # bold turns foreground light grey into white + if fg == 7 and not self.dim and self.bold: + fg = 15 + + # now compute CSS classes + classes = [] + if isinstance(fg, int): + if fg != self.default_fg: + classes.append(self.COLORS[fg]) + else: + # 256-color palette + classes.append(fg[0]) + + if isinstance(bg, int): + if bg != self.default_bg: + classes.append('B' + self.COLORS[bg]) + else: + classes.append(bg[1]) + + if self.bold: + classes.append('BOLD') + if self.italic: + classes.append('ITA') + + if self.underline or self.strike: + undstr = '' + if self.underline: + undstr += 'UND' + if self.strike: + undstr += 'STR' + classes.append(undstr) + + self.cur_class = self._class_to_id(" ".join(classes)) + + def _write_span(self, text, class_id): + if class_id > 0: + yield ('<span class="%s">' % self.id_to_class[class_id]) + yield self.RE_ENTITIES.sub(lambda x: self.ENTITIES[x.group(0)], text) + if class_id > 0: + yield '</span>' + + def _write_form_feed(self): + yield '<hr>' + + def convert(self, input): + yield from self._write_prefix() + yield from super(ANSI2HTMLConverter, self).convert(input) + def finish(self): yield from self._write_prefix() - yield from self._write_line('</pre>') + yield from super(ANSI2HTMLConverter, self).finish() + yield '</pre>' self.prefix = '<pre class="ansi">' - self._reset_attrs() + def ansi2html(input, white_bg=False): c = ANSI2HTMLConverter(white_bg=white_bg) -- 2.14.3 _______________________________________________ Patchew-devel mailing list Patchew-devel@redhat.com https://www.redhat.com/mailman/listinfo/patchew-devel
This converter compresses sequences involving carriage returns or backspaces, to make them readable from an editor or mail reader. Signed-off-by: Paolo Bonzini <pbonzini@redhat.com> --- patchew/logviewer.py | 29 ++++++++++++++++++ patchew/settings.py | 3 ++ patchew/tags.py | 19 ++++++++++++ tests/test_ansi2html.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++- tests/test_custom_tags.py | 27 +++++++++++++++++ 5 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 patchew/tags.py create mode 100755 tests/test_custom_tags.py diff --git a/patchew/logviewer.py b/patchew/logviewer.py index XXXXXXX..XXXXXXX 100644 --- a/patchew/logviewer.py +++ b/patchew/logviewer.py @@ -XXX,XX +XXX,XX @@ class ANSIProcessor(object): self._reset_attrs() +class ANSI2TextConverter(ANSIProcessor): + FF = '\u2500' * 72 + '\n' + SYMBOLS = { + '\x00' : '\u2400', '\x01' : '\u2401', '\x02' : '\u2402', + '\x03' : '\u2403', '\x04' : '\u2404', '\x05' : '\u2405', + '\x06' : '\u2406', '\x07' : '\U00001F514', '\x0B' : '\u240B', + '\x0E' : '\u240E', '\x0F' : '\u240F', '\x10' : '\u2410', + '\x11' : '\u2411', '\x12' : '\u2412', '\x13' : '\u2413', + '\x14' : '\u2414', '\x15' : '\u2415', '\x16' : '\u2416', + '\x17' : '\u2417', '\x18' : '\u2418', '\x19' : '\u2419', + '\x1A' : '\u241A', '\x1B' : '\u241B', '\x1C' : '\u241C', + '\x1D' : '\u241D', '\x1E' : '\u241E', '\x1F' : '\u241F', + '\x7F' : '\u2326' + } + RE_SYMBOLS = re.compile('[\x00-\x1F\x7F]') + + def _write_span(self, text, class_id): + yield self.RE_SYMBOLS.sub(lambda x: self.SYMBOLS[x.group(0)], text) + + def _write_form_feed(self): + yield self.FF + + class ANSI2HTMLConverter(ANSIProcessor): ENTITIES = { '\x00' : '␀', '\x01' : '␁', '\x02' : '␂', @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLConverter(ANSIProcessor): self.prefix = '<pre class="ansi">' +def ansi2text(input): + c = ANSI2TextConverter() + yield from c.convert(input) + yield from c.finish() + + def ansi2html(input, white_bg=False): c = ANSI2HTMLConverter(white_bg=white_bg) yield from c.convert(input) diff --git a/patchew/settings.py b/patchew/settings.py index XXXXXXX..XXXXXXX 100644 --- a/patchew/settings.py +++ b/patchew/settings.py @@ -XXX,XX +XXX,XX @@ TEMPLATES = [ 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], + 'builtins': [ + 'patchew.tags', + ], }, }, ] diff --git a/patchew/tags.py b/patchew/tags.py new file mode 100644 index XXXXXXX..XXXXXXX --- /dev/null +++ b/patchew/tags.py @@ -XXX,XX +XXX,XX @@ +#!/usr/bin/env python3 +# +# Copyright 2018 Red Hat, Inc. +# +# Authors: +# Paolo Bonzini <pbonzini@redhat.com> +# +# This work is licensed under the MIT License. Please see the LICENSE file or +# http://opensource.org/licenses/MIT. + +from django import template +from patchew import logviewer + +register = template.Library() + +@register.simple_tag +@register.filter +def ansi2text(value): + return ''.join(logviewer.ansi2text(value)) diff --git a/tests/test_ansi2html.py b/tests/test_ansi2html.py index XXXXXXX..XXXXXXX 100644 --- a/tests/test_ansi2html.py +++ b/tests/test_ansi2html.py @@ -XXX,XX +XXX,XX @@ import unittest -from patchew.logviewer import ansi2html +from patchew.logviewer import ansi2html, ansi2text, ANSI2TextConverter class ANSI2HTMLTest(unittest.TestCase): def assertAnsi(self, test, expected, **kwargs): @@ -XXX,XX +XXX,XX @@ class ANSI2HTMLTest(unittest.TestCase): self.assertWhiteBg('abc\x1b[7m\x1b[1Kabc', ' <span class="WHI BBLK">abc</span>') +class ANSI2TextTest(unittest.TestCase): + def assertAnsi(self, test, expected, **kwargs): + self.assertEqual(''.join(ansi2text(test, **kwargs)), expected, + repr(test)) + + # basic formatting tests + def test_basic(self): + self.assertAnsi('\tb', ' b') + self.assertAnsi('\t\ta', ' a') + self.assertAnsi('a\tb', 'a b') + self.assertAnsi('ab\tc', 'ab c') + self.assertAnsi('a\nbc', 'a\nbc') + self.assertAnsi('a\f', 'a\n' + ANSI2TextConverter.FF) + self.assertAnsi('a\n\f', 'a\n\n' + ANSI2TextConverter.FF) + self.assertAnsi('<', '<') + self.assertAnsi('\x07', '\U00001F514') + + # backspace and carriage return + def test_set_pos(self): + self.assertAnsi('abc\b\bBC', 'aBC') + self.assertAnsi('a\b<', '<') + self.assertAnsi('<\ba', 'a') + self.assertAnsi('a\b\bbc', 'bc') + self.assertAnsi('a\rbc', 'bc') + self.assertAnsi('a\nb\bc', 'a\nc') + self.assertAnsi('a\t\bb', 'a b') + self.assertAnsi('a\tb\b\bc', 'a cb') + self.assertAnsi('01234567\r\tb', '01234567b') + + # Escape sequences + def test_esc_parsing(self): + self.assertAnsi('{\x1b%}', '{}') + self.assertAnsi('{\x1b[0m}', '{}') + self.assertAnsi('{\x1b[m}', '{}') + self.assertAnsi('{\x1b[0;1;7;0m}', '{}') + self.assertAnsi('{\x1b[1;7m\x1b[m}', '{}') + self.assertAnsi('{\x1b]test\x1b\\}', '{}') + self.assertAnsi('{\x1b]test\x07}', '{}') + self.assertAnsi('{\x1b]test\x1b[0m\x07}', '{}') + self.assertAnsi('{\x1b]test\x1b[7m\x07}', '{}') + + # ESC [C and ESC [D + def test_horiz_movement(self): + self.assertAnsi('abc\x1b[2DB', 'aBc') + self.assertAnsi('abc\x1b[3CD', 'abc D') + self.assertAnsi('abcd\x1b[3DB\x1b[1CD', 'aBcD') + self.assertAnsi('abc\x1b[0CD', 'abc D') + self.assertAnsi('abc\x1b[CD', 'abc D') + + # ESC [K + def test_clear_line(self): + self.assertAnsi('\x1b[Kabcd', 'abcd') + self.assertAnsi('abcd\r\x1b[K', '') + self.assertAnsi('abcd\b\x1b[K', 'abc') + self.assertAnsi('abcd\r\x1b[KDef', 'Def') + self.assertAnsi('abcd\b\x1b[KDef', 'abcDef') + self.assertAnsi('abcd\r\x1b[0K', '') + self.assertAnsi('abcd\b\x1b[0K', 'abc') + self.assertAnsi('abcd\r\x1b[1K', 'abcd') + self.assertAnsi('abcd\b\x1b[1K', ' d') + self.assertAnsi('abcd\r\x1b[2K', '') + self.assertAnsi('abcd\b\x1b[2K', ' ') + self.assertAnsi('abcd\r\x1b[2KDef', 'Def') + self.assertAnsi('abcd\b\x1b[2KDef', ' Def') + + # combining cursor movement and formatting + def test_movement_and_formatting(self): + self.assertAnsi('\x1b[42m\tabc', ' abc') + self.assertAnsi('abc\x1b[42m\x1b[1Kabc', ' abc') + self.assertAnsi('\x1b[7m\tabc', ' abc') + self.assertAnsi('abc\x1b[7m\x1b[1Kabc', ' abc') + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_custom_tags.py b/tests/test_custom_tags.py new file mode 100755 index XXXXXXX..XXXXXXX --- /dev/null +++ b/tests/test_custom_tags.py @@ -XXX,XX +XXX,XX @@ +#!/usr/bin/env python3 +# +# Copyright 2018 Red Hat, Inc. +# +# Authors: +# Paolo Bonzini <pbonzini@redhat.com> +# +# This work is licensed under the MIT License. Please see the LICENSE file or +# http://opensource.org/licenses/MIT. + +from django.template import Context, Template +import patchewtest +import unittest + +class CustomTagsTest(unittest.TestCase): + def assertTemplate(self, template, expected, **kwargs): + context = Context(kwargs) + self.assertEqual(Template(template).render(context), expected) + + def test_template_filters(self): + self.assertTemplate('{{s|ansi2text}}', 'dbc', s='abc\rd') + + def test_template_tags(self): + self.assertTemplate('{% ansi2text s %}', 'dbc', s='abc\rd') + +if __name__ == '__main__': + unittest.main() -- 2.14.3 _______________________________________________ Patchew-devel mailing list Patchew-devel@redhat.com https://www.redhat.com/mailman/listinfo/patchew-devel
These can be used to make test report emails shorter and more readable. Signed-off-by: Paolo Bonzini <pbonzini@redhat.com> --- patchew/tags.py | 77 ++++++++++++++++++++++++++++++++++ tests/test_custom_tags.py | 102 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+) diff --git a/patchew/tags.py b/patchew/tags.py index XXXXXXX..XXXXXXX 100644 --- a/patchew/tags.py +++ b/patchew/tags.py @@ -XXX,XX +XXX,XX @@ from django import template from patchew import logviewer +from collections import deque +import io +import operator +import re + +# The basic implementation uses generators. The filters simply apply join +# to the result of the generators. + +def lines_iter(value): + if not isinstance(value, io.IOBase): + # StringIO provides a generator to split lines. + value = io.StringIO(value) + # "chomp" the newlines on each line. Using operator and map does + # everything in the interpreter, avoiding the overhead of a generator. + return map(operator.methodcaller('rstrip', '\r\n'), value) + +# To understand grep_iter, it may help to first study this implementation +# of a "tail" iterator, which is based on the same circular array idea: +# +# def tail_lines_iter(value, n): +# lines = [None] * n +# lineno = 0 +# for line in lines_iter(value): +# lines[lineno % n] = line +# lineno += 1 +# +# for i in range(max(lineno - n, 0), lineno): +# yield lines[i % n] +# +# Basic "grep" prints one line when the match is on the last line, so +# "grep" is a variation on tail_lines with n=1; likewise, "grep -B1" is +# a variation on tail_lines with n=2, etc. + +def grep_iter(value, regex, n_before, n_after, sep): + n = n_before + 1 + lines = [None] * n + stop = lineno = 0 + for line in lines_iter(value): + # Print the (lineno - n)-th line. Each element of lines[] is used + # just before it is thrown away. + if lineno - n >= 0 and lineno - n < stop: + yield lines[lineno % n] + if re.search(regex, line): + if lineno - n >= stop and sep is not None and stop > 0: + yield sep + stop = lineno + n_after + 1 + lines[lineno % n] = line + lineno += 1 + + for i in range(max(lineno - n, 0), min(stop, lineno)): + yield lines[i % n] register = template.Library() @@ -XXX,XX +XXX,XX @@ register = template.Library() @register.filter def ansi2text(value): return ''.join(logviewer.ansi2text(value)) + +@register.simple_tag +@register.filter +def tail_lines(value, n): + lines = deque(lines_iter(value), n) + return '\n'.join(lines) + +@register.simple_tag +@register.filter +def grep(value, regex, sep=None): + return '\n'.join(grep_iter(value, regex, 0, 0, sep)) + +@register.simple_tag +@register.filter +def grep_A(value, regex, n=3, sep='---'): + return '\n'.join(grep_iter(value, regex, 0, n, sep)) + +@register.simple_tag +@register.filter +def grep_B(value, regex, n=3, sep='---'): + return '\n'.join(grep_iter(value, regex, n, 0, sep)) + +@register.simple_tag +@register.filter +def grep_C(value, regex, n=3, sep='---'): + return '\n'.join(grep_iter(value, regex, n, n, sep)) diff --git a/tests/test_custom_tags.py b/tests/test_custom_tags.py index XXXXXXX..XXXXXXX 100755 --- a/tests/test_custom_tags.py +++ b/tests/test_custom_tags.py @@ -XXX,XX +XXX,XX @@ # http://opensource.org/licenses/MIT. from django.template import Context, Template +from patchew.tags import tail_lines, grep_A, grep_B, grep_C, grep import patchewtest import unittest @@ -XXX,XX +XXX,XX @@ class CustomTagsTest(unittest.TestCase): def test_template_filters(self): self.assertTemplate('{{s|ansi2text}}', 'dbc', s='abc\rd') + self.assertTemplate('{{s|grep:"[0-9]"}}', '0\n9', s='0\na\n9') + self.assertTemplate('{{s|grep_A:"b"}}', + 'b\nc\nd\ne\n---\nb', + s='a\nb\nc\nd\ne\nf\nx\ny\nz\nb') + self.assertTemplate('{{s|grep_B:"b"}}', + 'a\nb\n---\nx\ny\nz\nb', + s='a\nb\nc\nd\ne\nf\nx\ny\nz\nb') + self.assertTemplate('{{s|grep_C:"b"}}', + 'a\nb\nc\nd\ne\n---\nx\ny\nz\nb', + s='a\nb\nc\nd\ne\nf\nx\ny\nz\nb') + self.assertTemplate('{{s|tail_lines:3}}', 'b\nc\nd', s='a\nb\nc\nd') def test_template_tags(self): self.assertTemplate('{% ansi2text s %}', 'dbc', s='abc\rd') + self.assertTemplate('{% grep s "[0-9]" %}', '0\n9', s='0\na\n9') + self.assertTemplate('{% grep_A s regex="[bc]" n=1 %}', 'b\nc\nd', s='a\nb\nc\nd') + self.assertTemplate('{% grep_B s regex="[bc]" n=1 %}', 'a\nb\nc', s='a\nb\nc\nd') + self.assertTemplate('{% grep_C s "b" n=1 %}', 'a\nb\nc', s='a\nb\nc\nd') + self.assertTemplate('{% tail_lines s n=3 %}', 'b\nc\nd', s='a\nb\nc\nd') + + def test_grep(self): + self.assertEqual(grep('0\na\n9', '[0-9]'), '0\n9') + self.assertEqual(grep('0\na\n9', '[0-9]', '---'), '0\n---\n9') + + def test_grep_A(self): + self.assertEqual(grep_A('a\nb\nc\nd', 'b', 1, None), 'b\nc') + self.assertEqual(grep_A('a\nb\nc\nd', 'b', 2, None), 'b\nc\nd') + self.assertEqual(grep_A('a\nb\nc\nd\nb\ne', 'b', 1, None), 'b\nc\nb\ne') + self.assertEqual(grep_A('a\nb\nc\nd\nb\ne', 'b', 2, None), 'b\nc\nd\nb\ne') + self.assertEqual(grep_A('a\nb\nc\nd\nz\nb\ne', 'b', 1, None), 'b\nc\nb\ne') + self.assertEqual(grep_A('a\nb\nc\nd\nz\nb\ne', 'b', 2, None), 'b\nc\nd\nb\ne') + self.assertEqual(grep_A('a\nb\nc\nz\nb\nb\ne', 'b', 1, None), 'b\nc\nb\nb\ne') + self.assertEqual(grep_A('b\nc\nz\nb\nb\ne', 'b', 1, None), 'b\nc\nb\nb\ne') + self.assertEqual(grep_A('b\n', 'b', 1, None), 'b') + + def test_grep_A_sep(self): + self.assertEqual(grep_A('a\nb\nc\nd', 'b', 1), 'b\nc') + self.assertEqual(grep_A('a\nb\nc\nd', 'b', 2), 'b\nc\nd') + self.assertEqual(grep_A('a\nb\nc\nd\nb\ne', 'b', 1), 'b\nc\n---\nb\ne') + self.assertEqual(grep_A('a\nb\nc\nd\nb\ne', 'b', 2), 'b\nc\nd\nb\ne') + self.assertEqual(grep_A('a\nb\nc\nd\nz\nb\ne', 'b', 1), 'b\nc\n---\nb\ne') + self.assertEqual(grep_A('a\nb\nc\nd\nz\nb\nb\ne', 'b', 1), 'b\nc\n---\nb\nb\ne') + self.assertEqual(grep_A('b\nc\nz\nb\nb\ne', 'b', 1), 'b\nc\n---\nb\nb\ne') + self.assertEqual(grep_A('b\n', 'b', 1), 'b') + + def test_grep_B(self): + self.assertEqual(grep_B('a\nb\nc\nd', 'b', 1, None), 'a\nb') + self.assertEqual(grep_B('a\nb\nc\nd', 'b', 2, None), 'a\nb') + self.assertEqual(grep_B('a\nb\nc\nd\nb\ne', 'b', 1, None), 'a\nb\nd\nb') + self.assertEqual(grep_B('a\nb\nc\nd\nz\nb\ne', 'b', 1, None), 'a\nb\nz\nb') + self.assertEqual(grep_B('a\nb\nc\nd\nz\nb\ne', 'b', 2, None), 'a\nb\nd\nz\nb') + self.assertEqual(grep_B('a\nb\nc\nz\nb\nb\ne', 'b', 1, None), 'a\nb\nz\nb\nb') + self.assertEqual(grep_B('b\nc\nz\nb\nb\ne', 'b', 1, None), 'b\nz\nb\nb') + self.assertEqual(grep_B('b\n', 'b', 1, None), 'b') + + def test_grep_B_sep(self): + self.assertEqual(grep_B('a\nb\nc\nd', 'b', 1), 'a\nb') + self.assertEqual(grep_B('a\nb\nc\nd', 'b', 2), 'a\nb') + self.assertEqual(grep_B('a\nb\nc\nd\nb\ne', 'b', 1), 'a\nb\n---\nd\nb') + self.assertEqual(grep_B('a\nb\nc\nd\nz\nb\ne', 'b', 1), 'a\nb\n---\nz\nb') + self.assertEqual(grep_B('a\nb\nc\nd\nz\nb\ne', 'b', 2), 'a\nb\n---\nd\nz\nb') + self.assertEqual(grep_B('a\nb\nc\nz\nb\nb\ne', 'b', 1), 'a\nb\n---\nz\nb\nb') + self.assertEqual(grep_B('b\nc\nz\nb\nb\ne', 'b', 1), 'b\n---\nz\nb\nb') + self.assertEqual(grep_B('b\n', 'b', 1), 'b') + + def test_grep_C(self): + self.assertEqual(grep_C('a\nb\nc\nd', 'b', 1, None), 'a\nb\nc') + self.assertEqual(grep_C('a\nb\nc\nd', 'b', 2, None), 'a\nb\nc\nd') + self.assertEqual(grep_C('a\nb\nc\nd\nb\ne', 'b', 1, None), 'a\nb\nc\nd\nb\ne') + self.assertEqual(grep_C('a\nb\nc\nd\nz\nb\ne', 'b', 1, None), 'a\nb\nc\nz\nb\ne') + self.assertEqual(grep_C('a\nb\nc\nd\nz\nb\ne', 'b', 2, None), 'a\nb\nc\nd\nz\nb\ne') + self.assertEqual(grep_C('a\nb\nc\nz\nb\nb\ne', 'b', 1, None), 'a\nb\nc\nz\nb\nb\ne') + self.assertEqual(grep_C('b\nc\nz\nb\nb\ne', 'b', 1, None), 'b\nc\nz\nb\nb\ne') + self.assertEqual(grep_C('b\n', 'b', 1, None), 'b') + + def test_grep_C_sep(self): + self.assertEqual(grep_C('a\nb\nc\nd', 'b', 1), 'a\nb\nc') + self.assertEqual(grep_C('a\nb\nc\nd', 'b', 2), 'a\nb\nc\nd') + self.assertEqual(grep_C('a\nb\nc\nd\nb\ne', 'b', 1), 'a\nb\nc\nd\nb\ne') + self.assertEqual(grep_C('a\nb\nc\nd\nz\nb\ne', 'b', 1), 'a\nb\nc\n---\nz\nb\ne') + self.assertEqual(grep_C('a\nb\nc\nd\nz\nb\ne', 'b', 2), 'a\nb\nc\nd\nz\nb\ne') + self.assertEqual(grep_C('a\nb\nc\nz\nb\nb\ne', 'b', 1), 'a\nb\nc\nz\nb\nb\ne') + self.assertEqual(grep_C('b\nc\nz\nb\nb\ne', 'b', 1), 'b\nc\nz\nb\nb\ne') + self.assertEqual(grep_C('b\n', 'b', 1), 'b') + + def test_tail_lines(self): + self.assertEqual(tail_lines('', 0), '') + self.assertEqual(tail_lines('', 1), '') + self.assertEqual(tail_lines('', 2), '') + self.assertEqual(tail_lines('', 4), '') + + self.assertEqual(tail_lines('a\nb\n', 0), '') + self.assertEqual(tail_lines('a\nb\n', 1), 'b') + self.assertEqual(tail_lines('a\nb\n', 2), 'a\nb') + + self.assertEqual(tail_lines('a\nb\nc\n', 2), 'b\nc') + self.assertEqual(tail_lines('a\nb\nc\n', 4), 'a\nb\nc') + + self.assertEqual(tail_lines('a\nb\nc\nd\n', 2), 'c\nd') + + self.assertEqual(tail_lines('\n\n\n', 2), '\n') + self.assertEqual(tail_lines('\n\n\nbc', 2), '\nbc') + self.assertEqual(tail_lines('\n\nbc', 3), '\n\nbc') + self.assertEqual(tail_lines('\n\n\n\nbc', 3), '\n\nbc') if __name__ == '__main__': unittest.main() -- 2.14.3 _______________________________________________ Patchew-devel mailing list Patchew-devel@redhat.com https://www.redhat.com/mailman/listinfo/patchew-devel
The request is needed by rest_framework.reverse.reverse. It is already passed to the plugins via PluginMethodField, pass it to the hooks as well. Signed-off-by: Paolo Bonzini <pbonzini@redhat.com> --- api/rest.py | 12 +++++++++--- mods/diff.py | 2 +- mods/git.py | 2 +- mods/tags.py | 2 +- mods/testing.py | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/api/rest.py b/api/rest.py index XXXXXXX..XXXXXXX 100644 --- a/api/rest.py +++ b/api/rest.py @@ -XXX,XX +XXX,XX @@ class SeriesSerializer(BaseMessageSerializer): def get_fields(self): fields = super(SeriesSerializer, self).get_fields() - dispatch_module_hook("rest_series_fields_hook", fields=fields, detailed=self.detailed) + request = self.context['request'] + dispatch_module_hook("rest_series_fields_hook", request=request, + fields=fields, detailed=self.detailed) return fields def get_results(self, message): results = {} - dispatch_module_hook("rest_results_hook", message=message, results=results) + request = self.context['request'] + dispatch_module_hook("rest_results_hook", request=request, + message=message, results=results) return results def get_total_patches(self, obj): @@ -XXX,XX +XXX,XX @@ class MessageSerializer(BaseMessageSerializer): def get_fields(self): fields = super(MessageSerializer, self).get_fields() - dispatch_module_hook("rest_message_fields_hook", fields=fields) + request = self.context['request'] + dispatch_module_hook("rest_message_fields_hook", request=request, + fields=fields) return fields class StaticTextRenderer(renderers.BaseRenderer): diff --git a/mods/diff.py b/mods/diff.py index XXXXXXX..XXXXXXX 100644 --- a/mods/diff.py +++ b/mods/diff.py @@ -XXX,XX +XXX,XX @@ class DiffModule(PatchewModule): for o in sorted(other_versions, key=lambda y: y.version) if o.message_id != message.message_id] - def rest_series_fields_hook(self, fields, detailed): + def rest_series_fields_hook(self, request, fields, detailed): fields['version'] = rest_framework.fields.IntegerField() if detailed: fields['other_versions'] = PluginMethodField(obj=self) diff --git a/mods/git.py b/mods/git.py index XXXXXXX..XXXXXXX 100644 --- a/mods/git.py +++ b/mods/git.py @@ -XXX,XX +XXX,XX @@ class GitModule(PatchewModule): raise Exception("Project git repo invalid: %s" % project_git) return upstream, branch - def rest_results_hook(self, message, results): + def rest_results_hook(self, request, message, results): l = message.get_property("git.apply-log") if l: if message.get_property("git.apply-failed"): diff --git a/mods/tags.py b/mods/tags.py index XXXXXXX..XXXXXXX 100644 --- a/mods/tags.py +++ b/mods/tags.py @@ -XXX,XX +XXX,XX @@ series cover letter, patch mail body and their replies. def get_tags(self, m, request, format): return m.get_property("tags", []) - def rest_message_fields_hook(self, fields): + def rest_message_fields_hook(self, request, fields): fields['tags'] = PluginMethodField(obj=self) def prepare_message_hook(self, request, message, detailed): diff --git a/mods/testing.py b/mods/testing.py index XXXXXXX..XXXXXXX 100644 --- a/mods/testing.py +++ b/mods/testing.py @@ -XXX,XX +XXX,XX @@ class TestingModule(PatchewModule): return reverse("testing-get-prop", kwargs={"project_or_series": obj.message_id}) - def rest_results_hook(self, message, results): + def rest_results_hook(self, request, message, results): for pn, p in message.get_properties().items(): if not pn.startswith("testing.report."): continue -- 2.14.3 _______________________________________________ Patchew-devel mailing list Patchew-devel@redhat.com https://www.redhat.com/mailman/listinfo/patchew-devel
Abstract the creation of testing log URLs. Signed-off-by: Paolo Bonzini <pbonzini@redhat.com> --- mods/testing.py | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/mods/testing.py b/mods/testing.py index XXXXXXX..XXXXXXX 100644 --- a/mods/testing.py +++ b/mods/testing.py @@ -XXX,XX +XXX,XX @@ class TestingModule(PatchewModule): TestingLogViewer.as_view(), name="testing-log")) + def reverse_testing_log(self, obj, test, request=None, html=False): + if isinstance(obj, Message): + log_url = reverse("testing-log", + kwargs={"project_or_series": obj.message_id, + "testing_name": test}) + "?type=message" + else: + assert(isinstance(obj, Project)) + log_url = reverse.reverse("testing-log", + kwargs={"project_or_series": obj.name, + "testing_name": test}) + "?type=project" + if html: + log_url += "&html=1" + # Generate a full URL, including the host and port, for use in email + # notifications and REST API responses. + if request: + log_url = request.build_absolute_uri(log_url) + return log_url + def add_test_report(self, user, project, tester, test, head, base, identity, passed, log, is_timeout): # Find a project or series depending on the test type and assign it to obj @@ -XXX,XX +XXX,XX @@ class TestingModule(PatchewModule): continue tn = pn[len("testing.report."):] failed = not p["passed"] - log_prop = "testing.log." + tn - if isinstance(obj, Message): - typearg = "type=message" - log_url = reverse("testing-log", - kwargs={"project_or_series": obj.message_id, - "testing_name": tn}) - else: - assert(isinstance(obj, Project)) - typearg = "type=project" - log_url = reverse("testing-log", - kwargs={"project_or_series": obj.name, - "testing_name": tn}) - log_url += "?" + typearg - html_log_url = log_url + "&html=1" + log_url = self.reverse_testing_log(obj, tn, html=False) + html_log_url = self.reverse_testing_log(obj, tn, html=True) passed_str = "failed" if failed else "passed" html = format_html('Test <b>{}</b> <a class="cbox-log" data-link="{}" href="{}">{}</a>', tn, html_log_url, log_url, passed_str) @@ -XXX,XX +XXX,XX @@ class TestingModule(PatchewModule): continue tn = pn[len("testing.report."):] failed = not p["passed"] - log_prop = "testing.log." + tn - typearg = "type=message" - log_url = reverse("testing-log", - kwargs={"project_or_series": message.message_id, - "testing_name": tn}) - log_url += "?" + typearg + log_url = self.reverse_testing_log(message, tn, request=request, html=False) passed_str = "failure" if failed else "success" result = { 'status': passed_str, -- 2.14.3 _______________________________________________ Patchew-devel mailing list Patchew-devel@redhat.com https://www.redhat.com/mailman/listinfo/patchew-devel
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com> --- mods/testing.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/mods/testing.py b/mods/testing.py index XXXXXXX..XXXXXXX 100644 --- a/mods/testing.py +++ b/mods/testing.py @@ -XXX,XX +XXX,XX @@ class TestingModule(PatchewModule): log_url = request.build_absolute_uri(log_url) return log_url - def add_test_report(self, user, project, tester, test, head, + def add_test_report(self, request, project, tester, test, head, base, identity, passed, log, is_timeout): # Find a project or series depending on the test type and assign it to obj if identity["type"] == "project": obj = Project.objects.get(name=project) - is_proj_report = True project = obj.name elif identity["type"] == "series": message_id = identity["message-id"] obj = Message.objects.find_series(message_id, project) if not obj: raise Exception("Series doesn't exist") - is_proj_report = False project = obj.project.name + user = request.user + log_url = self.reverse_testing_log(obj, test, request=request) + html_log_url = self.reverse_testing_log(obj, test, request=request, html=True) obj.set_property("testing.report." + test, {"passed": passed, "is_timeout": is_timeout, @@ -XXX,XX +XXX,XX @@ class TestingModule(PatchewModule): if all_tests.issubset(done_tests): obj.set_property("testing.tested-head", head) emit_event("TestingReport", tester=tester, user=user.username, - obj=obj, passed=passed, test=test, log=log, - is_timeout=is_timeout) + obj=obj, passed=passed, test=test, log=log, log_url=log_url, + html_log_url=html_log_url, is_timeout=is_timeout) def get_tests(self, obj): ret = {} @@ -XXX,XX +XXX,XX @@ class TestingReportView(APILoginRequiredView): head, base, passed, log, identity, is_timeout=False): _instance.tester_check_in(project, tester or request.user.username) - _instance.add_test_report(request.user, project, tester, + _instance.add_test_report(request, project, tester, test, head, base, identity, passed, log, is_timeout) -- 2.14.3 _______________________________________________ Patchew-devel mailing list Patchew-devel@redhat.com https://www.redhat.com/mailman/listinfo/patchew-devel