gtk: Create a MessageView

The message view will be used to display either stdout or stderr
messages to the user. It has built-in 'diff' detection, and adds nice
colors to the diff output if we are asked to display one.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
This commit is contained in:
Anna Schumaker 2023-08-30 16:57:38 -04:00
parent 54be8f5ce3
commit 5fb9bd6221
2 changed files with 162 additions and 0 deletions

View File

@ -257,6 +257,101 @@ class TestTestCaseView(unittest.TestCase):
"stdout", "stderr") "stdout", "stderr")
class TestMessageView(unittest.TestCase):
"""Test the MessageView."""
def setUp(self):
"""Set up common variables."""
self.view = xfstestsdb.gtk.view.MessageView("title")
def test_init(self):
"""Check that the MessageView was set up correctly."""
self.assertIsInstance(self.view, Gtk.Box)
self.assertEqual(self.view.props.orientation, Gtk.Orientation.VERTICAL)
self.assertTrue(self.view.has_css_class("view"))
def test_detect_diff(self):
"""Check detecting if input test looks like a diff."""
self.assertFalse(self.view.detect_diff("not a diff"))
lines = ["+++ /some/file"]
self.assertFalse(self.view.detect_diff("\n".join(lines)))
lines.append("--- /some/other/file")
self.assertFalse(self.view.detect_diff("\n".join(lines)))
lines.append("@@ 12,34,5 @@")
self.assertFalse(self.view.detect_diff("\n".join(lines)))
lines.append(" some context line")
self.assertTrue(self.view.detect_diff("\n".join(lines)))
lines[-1] = "+an added line"
self.assertTrue(self.view.detect_diff("\n".join(lines)))
lines[-1] = "-a removed line"
self.assertTrue(self.view.detect_diff("\n".join(lines)))
def test_markup_diff(self):
"""Check colorizing lines with diff colors."""
self.assertEqual(self.view.markup_diff("abcde"), "abcde")
self.assertEqual(self.view.markup_diff("+++ /some/file"),
"<span color='#26a269'>+++ /some/file</span>")
self.assertEqual(self.view.markup_diff("--- /some/other/file"),
"<span color='#c01c28'>--- /some/other/file</span>")
self.assertEqual(self.view.markup_diff("@@ 12,34,5 @@"),
"<span color='#1c71d8'>@@ 12,34,5 @@</span>")
self.assertEqual(self.view.markup_diff(" a context line"),
"<span color='#77767b'> a context line</span>")
self.assertEqual(self.view.markup_diff("+an added line"),
"<span color='#26a269'>+an added line</span>")
self.assertEqual(self.view.markup_diff("-a removed line"),
"<span color='#c01c28'>-a removed line</span>")
def test_title(self):
"""Test the title widgets."""
self.assertIsInstance(self.view._label, Gtk.Label)
self.assertIsInstance(self.view._label.get_next_sibling(),
Gtk.Separator)
self.assertEqual(self.view.get_first_child(), self.view._label)
self.assertEqual(self.view.title, "title")
self.assertEqual(self.view._label.props.label, "title")
self.assertEqual(self.view._label.props.margin_top, 6)
self.assertTrue(self.view._label.has_css_class("large-title"))
def test_text(self):
"""Test the text property."""
win = self.view.get_last_child()
self.assertIsInstance(win, Gtk.ScrolledWindow)
self.assertIsInstance(self.view._textview, Gtk.TextView)
self.assertEqual(win.props.child, self.view._textview)
self.assertTrue(win.props.vexpand)
self.assertFalse(self.view._textview.props.editable)
self.assertTrue(self.view._textview.props.monospace)
buffer = self.view._textview.get_buffer()
self.assertEqual(self.view.text, "")
with unittest.mock.patch.object(buffer, "set_text",
wraps=buffer.set_text) as mock_set:
self.view.text = "text"
self.assertEqual(buffer.get_text(buffer.get_start_iter(),
buffer.get_end_iter(), True),
"text")
mock_set.assert_called_with("text")
self.assertEqual(self.view.text, "text")
def test_text_diff(self):
"""Test setting the text property to a diff string."""
buffer = self.view._textview.get_buffer()
diff = ["+++ /some/file", "--- /some/other/file", "@@ 12,34,5 @@",
" context line", "-removed line", "+added line"]
with unittest.mock.patch.object(buffer, "set_text",
wraps=buffer.set_text) as mock_set:
self.view.text = "\n".join(diff)
mock_set.assert_not_called()
self.assertEqual(self.view.text, "\n".join(diff))
self.view.text = "\n".join(diff)
self.assertEqual(self.view.text, "\n".join(diff))
class TestSummaryView(unittest.TestCase): class TestSummaryView(unittest.TestCase):
"""Tests the SummaryView.""" """Tests the SummaryView."""

View File

@ -1,5 +1,6 @@
# Copyright 2023 (c) Anna Schumaker. # Copyright 2023 (c) Anna Schumaker.
"""A view widget used to display our TestCaseModel.""" """A view widget used to display our TestCaseModel."""
import re
from gi.repository import GObject from gi.repository import GObject
from gi.repository import Gio from gi.repository import Gio
from gi.repository import Gtk from gi.repository import Gtk
@ -164,6 +165,72 @@ class TestCaseView(XunitView):
"""Signal that the user wants to inspect stdout and stderr messages.""" """Signal that the user wants to inspect stdout and stderr messages."""
class MessageView(Gtk.Box):
"""A view for displaying a multiline test result message."""
title = GObject.Property(type=str)
def __init__(self, title: str):
"""Initialize a MessageView."""
super().__init__(title=title, orientation=Gtk.Orientation.VERTICAL)
self._label = Gtk.Label(label=self.title, margin_top=6)
self._textview = Gtk.TextView(monospace=True, editable=False)
self.append(self._label)
self.append(Gtk.Separator())
self.append(Gtk.ScrolledWindow(child=self._textview, vexpand=True))
self._label.add_css_class("large-title")
self.add_css_class("view")
def detect_diff(self, text: str) -> bool:
"""Detect if the given text looks like a diff."""
in_file = out_file = counts = changed = False
for line in text.split("\n"):
if re.match(r"^\+\+\+", line):
in_file = True
elif re.match(r"^---", line):
out_file = True
elif re.match(r"^@@(.*?)@@", line):
counts = True
elif re.match(r"^[\+| |-](.*?)", line):
changed = True
return in_file and out_file and counts and changed
def markup_diff(self, text: str) -> str:
"""Add Pango markup to the input string."""
if re.match(r"^\++(.*?)", text):
return f"<span color='#26a269'>{text}</span>"
elif re.match(r"^-+(.*?)", text):
return f"<span color='#c01c28'>{text}</span>"
elif re.match(r"^@@(.*?)@@", text):
return f"<span color='#1c71d8'>{text}</span>"
elif re.match(r"^ (.*?)", text):
return f"<span color='#77767b'>{text}</span>"
return text
@GObject.Property(type=str)
def text(self) -> str:
"""Get the text displayed in the view."""
buffer = self._textview.props.buffer
return buffer.get_text(buffer.get_start_iter(),
buffer.get_end_iter(), True)
@text.setter
def text(self, new_text: str) -> None:
buffer = self._textview.props.buffer
if self.detect_diff(new_text):
buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
for i, line in enumerate(new_text.split("\n")):
text = self.markup_diff(line)
text = f"\n{text}" if i > 0 else text
buffer.insert_markup(buffer.get_end_iter(), text, len(text))
else:
buffer.set_text(new_text)
class SummaryView(XunitView): class SummaryView(XunitView):
"""Displays our SummaryList model to the user.""" """Displays our SummaryList model to the user."""