# 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"{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 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