xfstestsdb/xfstestsdb/gtk/view.py

347 lines
13 KiB
Python

# 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
from gi.repository import Adw
from .model import PropertyList
from .model import PropertyFilter
from .model import TestCaseList
from .model import TestCaseFilter
from .model import SummaryList
from .model import XunitList
from . import button
from . import row
class XunitView(Gtk.ScrolledWindow):
"""Our XunitView base class."""
filtermodel = GObject.Property(type=Gtk.FilterListModel)
firstcol = GObject.Property(type=Gtk.ColumnViewColumn)
def __init__(self, title: str, **kwargs):
"""Initialize an XunitView."""
factory = row.LabelFactory("name")
super().__init__(filtermodel=Gtk.FilterListModel(),
firstcol=Gtk.ColumnViewColumn(title=title,
factory=factory),
child=Gtk.ColumnView(model=Gtk.NoSelection(),
show_row_separators=True,
show_column_separators=True,
hexpand=True),
**kwargs)
self.props.child.get_model().set_model(self.filtermodel)
self.add_css_class("undershoot-top")
self.add_css_class("undershoot-bottom")
self.add_css_class("card")
def do_make_factory(self, xunit: str) -> row.XunitFactory:
"""Make an XunitFactory for the given xunit."""
return row.XunitFactory(xunit=xunit)
@GObject.Property(type=XunitList)
def model(self) -> XunitList | None:
"""Get the XunitList shown by the XunitView."""
return self.filtermodel.props.model
@model.setter
def model(self, new: XunitList) -> None:
for col in list(self.props.child.get_columns()):
self.props.child.remove_column(col)
self.filtermodel.props.model = new
if new is not None:
self.props.child.append_column(self.firstcol)
for xunit in new.get_xunits():
col = Gtk.ColumnViewColumn(title=xunit, expand=True,
factory=self.do_make_factory(xunit))
self.props.child.append_column(col)
class EnvironmentView(XunitView):
"""Displays our Environment properties to the user."""
def __init__(self):
"""Initialize an EnvironmentView."""
super().__init__("environment", visible=False)
self.firstcol = None
self.props.child.add_css_class("data-table")
def do_make_factory(self, property: str) -> row.EnvironmentFactory:
"""Make a new EnvironmentFactory instance."""
return row.EnvironmentFactory(property=property)
@GObject.Property(type=Gio.ListModel)
def model(self) -> Gio.ListModel | None:
"""Get the ListModel shown by the EnvironmentView."""
return self.filtermodel.props.model
@model.setter
def model(self, new: Gio.ListModel) -> None:
for col in list(self.props.child.get_columns()):
self.props.child.remove_column(col)
self.filtermodel.props.model = new
self.props.visible = new is not None
if new is not None:
environ = new[0].get_environment()
for prop in environ.keys():
col = Gtk.ColumnViewColumn(title=prop, expand=True,
factory=self.do_make_factory(prop))
self.props.child.append_column(col)
class PropertyView(XunitView):
"""Displays our PropertyList model to the user."""
def __init__(self):
"""Initialize a PropertyView."""
super().__init__("property", vscrollbar_policy=Gtk.PolicyType.NEVER)
self.filtermodel.set_filter(PropertyFilter())
self.props.child.add_css_class("data-table")
def do_make_factory(self, xunit: str) -> row.PropertyFactory:
"""Make a new PropertyFactory instance."""
return row.PropertyFactory(xunit=xunit)
class FilterButtons(Gtk.Box):
"""Buttons for controlling the TestCaseFilter."""
passed = GObject.Property(type=bool, default=False)
skipped = GObject.Property(type=bool, default=False)
failure = GObject.Property(type=bool, default=True)
def __init__(self):
"""Initialize the FilterButtons."""
super().__init__()
self.add_css_class("linked")
self._passed = button.StatusToggle("test-pass", "passed")
self._skipped = button.StatusToggle("test-skip", "skipped")
self._failure = button.StatusToggle("test-fail", "failure",
active=True)
self._passed.bind_property("active", self, "passed")
self._skipped.bind_property("active", self, "skipped")
self._failure.bind_property("active", self, "failure")
self.append(self._passed)
self.append(self._skipped)
self.append(self._failure)
class TestCaseView(XunitView):
"""Displays our TestCaseList model to the user."""
filterbuttons = GObject.Property(type=FilterButtons)
def __init__(self):
"""Initialize a TestCaseView."""
super().__init__("testcase", filterbuttons=FilterButtons())
self.filtermodel.props.filter = TestCaseFilter()
for prop in ["passed", "skipped", "failure"]:
self.filterbuttons.bind_property(prop,
self.filtermodel.props.filter,
prop)
self.props.child.set_vexpand(True)
def __show_messages(self, factory: row.ResultFactory, testcase: str,
xunit: str, stdout: str, stderr: str) -> None:
self.emit("show-messages", testcase, xunit, stdout, stderr)
def do_make_factory(self, xunit: str) -> row.ResultFactory:
"""Make a new ResultFactory instance."""
factory = row.ResultFactory(xunit=xunit)
factory.connect("show-messages", self.__show_messages)
return factory
@GObject.Signal(arg_types=(str, str, str, str))
def show_messages(self, testcase: str, xunit: str,
stdout: str, stderr: str) -> None:
"""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 MessagesView(Gtk.Box):
"""A view for displaying stdout and stderr messages."""
testcase = GObject.Property(type=str)
xunit = GObject.Property(type=str)
stdout = GObject.Property(type=str)
stderr = GObject.Property(type=str)
def __init__(self):
"""Initialize a MessagesView."""
icon = "down-large-symbolic"
super().__init__(orientation=Gtk.Orientation.VERTICAL, margin_top=24,
margin_start=24, margin_end=24, margin_bottom=24)
self._back = Gtk.Button(child=Adw.ButtonContent(icon_name=icon,
label="done"))
self._title = Adw.WindowTitle()
self._stdout = MessageView("stdout")
self._stderr = MessageView("stderr")
self.bind_property("testcase", self._title, "title")
self.bind_property("xunit", self._title, "subtitle")
self.bind_property("stdout", self._stdout, "text")
self.bind_property("stderr", self._stderr, "text")
self._back.connect("clicked", self.__back_clicked)
self.append(Gtk.CenterBox(start_widget=self._back,
center_widget=self._title))
self.append(Gtk.Paned(start_child=self._stdout,
end_child=self._stderr, vexpand=True))
self.get_first_child().add_css_class("toolbar")
self._back.add_css_class("suggested-action")
self._back.add_css_class("pill")
self.add_css_class("card")
def __back_clicked(self, button: Gtk.Button) -> None:
self.emit("go-back")
@GObject.Signal
def go_back(self) -> None:
"""Signal that the user wants to go back."""
class SummaryView(XunitView):
"""Displays our SummaryList model to the user."""
def __init__(self):
"""Initialize a SummaryView."""
super().__init__("summary", vscrollbar_policy=Gtk.PolicyType.NEVER)
self.props.child.add_css_class("data-table")
def do_make_factory(self, xunit: str) -> row.SummaryFactory:
"""Make a new SummaryFactory instance."""
return row.SummaryFactory(xunit=xunit)
class XfstestsView(Gtk.Box):
"""A widget to display the results of an Xfstests runs."""
environment = GObject.Property(type=Gio.ListModel)
properties = GObject.Property(type=PropertyList)
model = GObject.Property(type=TestCaseList)
summary = GObject.Property(type=SummaryList)
def __init__(self):
"""Initialize an XfstestsView."""
animation = Gtk.StackTransitionType.OVER_UP_DOWN
super().__init__(orientation=Gtk.Orientation.VERTICAL)
self._environview = EnvironmentView()
self._propertyview = PropertyView()
self._testcaseview = TestCaseView()
self._messagesview = MessagesView()
self._stack = Gtk.Stack(transition_type=animation)
self._summaryview = SummaryView()
self.bind_property("environment", self._environview, "model")
self.bind_property("properties", self._propertyview, "model")
self.bind_property("model", self._testcaseview, "model")
self.bind_property("summary", self._summaryview, "model")
self._testcaseview.connect("show-messages", self.__show_messages)
self._messagesview.connect("go-back", self.__show_testcases)
self._stack.add_named(self._testcaseview, "testcases")
self._stack.add_named(self._messagesview, "messages")
self.append(self._environview)
self.append(Gtk.Separator())
self.append(self._propertyview)
self.append(Gtk.Separator())
self.append(self._stack)
self.append(Gtk.Separator())
self.append(self._summaryview)
def __show_messages(self, view: TestCaseView, testcase: str,
xunit: str, stdout: str, stderr: str) -> None:
self._messagesview.testcase = testcase
self._messagesview.xunit = xunit
self._messagesview.stdout = stdout
self._messagesview.stderr = stderr
self._stack.set_visible_child_name("messages")
def __show_testcases(self, view: MessagesView) -> None:
self._stack.set_visible_child_name("testcases")
@GObject.Property(type=FilterButtons)
def filterbuttons(self) -> FilterButtons:
"""Get the FilterButtons attached to the child TestCaseView."""
return self._testcaseview.filterbuttons