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")
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):
"""Tests the SummaryView."""

View File

@ -1,5 +1,6 @@
# Copyright 2023 (c) Anna Schumaker.
"""A view widget used to display our TestCaseModel."""
import re
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
@ -164,6 +165,72 @@ class TestCaseView(XunitView):
"""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):
"""Displays our SummaryList model to the user."""