From 5fb9bd6221153d2389f025495adb839212b7beec Mon Sep 17 00:00:00 2001 From: Anna Schumaker Date: Wed, 30 Aug 2023 16:57:38 -0400 Subject: [PATCH] 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 --- tests/gtk/test_view.py | 95 ++++++++++++++++++++++++++++++++++++++++++ xfstestsdb/gtk/view.py | 67 +++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) diff --git a/tests/gtk/test_view.py b/tests/gtk/test_view.py index fda4917..cd76f0d 100644 --- a/tests/gtk/test_view.py +++ b/tests/gtk/test_view.py @@ -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"), + "+++ /some/file") + self.assertEqual(self.view.markup_diff("--- /some/other/file"), + "--- /some/other/file") + self.assertEqual(self.view.markup_diff("@@ 12,34,5 @@"), + "@@ 12,34,5 @@") + self.assertEqual(self.view.markup_diff(" a context line"), + " a context line") + self.assertEqual(self.view.markup_diff("+an added line"), + "+an added line") + self.assertEqual(self.view.markup_diff("-a removed line"), + "-a removed line") + + 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.""" diff --git a/xfstestsdb/gtk/view.py b/xfstestsdb/gtk/view.py index 3ff7438..bc4d587 100644 --- a/xfstestsdb/gtk/view.py +++ b/xfstestsdb/gtk/view.py @@ -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"{text}" + elif re.match(r"^-+(.*?)", text): + return f"{text}" + elif re.match(r"^@@(.*?)@@", text): + return f"{text}" + elif re.match(r"^ (.*?)", text): + return f"{text}" + 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."""