#!/usr/bin/python import itertools import re from . import testresults from gi.repository import GObject from gi.repository import Gio from gi.repository import Gtk CSS = """ @define-color darkorange #cc7000; @define-color darkred #8b0000; label.passing-test { background-color: green; padding: 4px; } label.skipped-test { background-color: darkorange; padding: 4px; } menubutton.failed-test>button { background: darkred; border-color: darkred; border-radius: 0px; padding: 0px; } """ def markup_tooltip(status, color, message): return f" Test {status} {message}" class Label(Gtk.Label): def __init__(self, text): Gtk.Label.__init__(self, xalign=0) self.set_text(text) class PassingTest(Gtk.Label): def __init__(self, test): Gtk.Label.__init__(self) self.add_css_class("passing-test") self.set_text(f"{test.time} seconds") self.set_tooltip_markup(markup_tooltip("Passed", "green", f"in {test.timestr()}")) class SkippedTest(Gtk.Label): def __init__(self, test): Gtk.Label.__init__(self) self.add_css_class("skipped-test") self.set_text("Skipped") self.set_tooltip_markup(markup_tooltip("Skipped", "#cc7000", test.message)) class TestOutput(Gtk.Box): def __init__(self, sysout, syserr): Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) self.add_css_class("popover") self.stack = Gtk.Stack(vexpand=True, transition_type=Gtk.StackTransitionType.OVER_LEFT_RIGHT) self.prepend(self.stack) self.prepend(Gtk.StackSwitcher(stack=self.stack)) self.add_page("system-out", sysout, visible=False) self.add_page("system-err", syserr, visible=True) self.set_size_request(900, 500) def add_page(self, name, buffer, visible): view = Gtk.TextView(monospace=True) view.set_buffer(buffer) self.stack.add_titled(Gtk.ScrolledWindow(child=view), name, name) if visible: self.stack.set_visible_child_name(name) class FailedTest(Gtk.MenuButton): def __init__(self, test): Gtk.MenuButton.__init__(self, label="Failed") self.set_tooltip_markup(markup_tooltip("Failed", "#8b0000", test.message)) self.set_popover(Gtk.Popover(child=TestOutput(test.system_out, test.system_err))) self.add_css_class("failed-test") class MissingTest(Gtk.Label): def __init__(self, test): Gtk.Label.__init__(self, sensitive=False) self.set_name("missing-test") self.set_text("Missing") self.set_tooltip_text("Test Data Is Missing") class Model(GObject.GObject, Gio.ListModel): def __init__(self, results): GObject.GObject.__init__(self) self.results = results self.columns = [ "Testcase" ] + self.results.versions self.tests = sorted(results.tests.keys()) def get_columns(self): return self.columns def do_get_item_type(self): return GObject.TYPE_PYOBJECT def do_get_n_items(self): return len(self.tests) def do_get_item(self, i): return self.results.tests[self.tests[i]] class Filter(Gtk.Filter): def __init__(self): Gtk.Filter.__init__(self) self.regex = re.compile("") def set_regex(self, text): change = Gtk.FilterChange.DIFFERENT if text in self.regex.pattern: change = Gtk.FilterChange.LESS_STRICT elif self.regex.pattern in text: change = Gtk.FilterChange.MORE_STRICT self.regex = re.compile(text, re.I) self.changed(change) def do_match(self, testcase): search = [ f"name={testcase.name}" ] for key in sorted(testcase.versions.keys()): keys = [ "status", key ] + key.split("-") match type(testcase[key]): case testresults.PassingTest: res = [ "passing", "passed" ] case testresults.SkippedTest: res = [ "skipped" ] search.append(f"message={testcase[key].message}") case testresults.FailedTest: res = [ "failed", "failing" ] search.append(f"message={testcase[key].message}") case _: res = [ "missing" ] for k in keys: for r in res: search.append(f"{k}={r}") return self.regex.search(" ".join(search)) != None class Factory(Gtk.SignalListItemFactory): def __init__(self, column): Gtk.SignalListItemFactory.__init__(self) self.column = column self.connect("bind", self.on_bind) self.connect("unbind", self.on_unbind) def on_bind(self, factory, listitem): testcase = listitem.get_item() if self.column == "Testcase": child = Label(testcase.name) else: result = testcase[self.column] match type(result): case testresults.PassingTest: child = PassingTest(result) case testresults.SkippedTest: child = SkippedTest(result) case testresults.FailedTest: child = FailedTest(result) case _: child = child = MissingTest(result) listitem.set_child(child) def on_unbind(self, factory, listitem): listitem.set_child(None) class Viewer(Gtk.ScrolledWindow): def __init__(self): Gtk.ScrolledWindow.__init__(self, hscrollbar_policy=Gtk.PolicyType.NEVER, hexpand=True, vexpand=True, margin_top=5) self.provider = Gtk.CssProvider() self.filter = Gtk.FilterListModel.new(filter=Filter()) self.selection = Gtk.NoSelection.new(self.filter) self.columnview = Gtk.ColumnView.new(model=self.selection) self.columnview.add_css_class("data-table") self.provider.load_from_data(CSS.encode()) self.get_style_context().add_provider_for_display(self.get_display(), self.provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) self.set_child(self.columnview) def get_filter(self): return self.filter.get_filter() def clear(self): columns = self.columnview.get_columns() columns = [ columns.get_item(i) for i in range(columns.get_n_items()) ] for col in columns: self.columnview.remove_column(col) self.filter.set_model(None) def show_results(self, results): if results: self.filter.set_model(Model(results)) for i, name in enumerate(self.filter.get_model().get_columns()): column = Gtk.ColumnViewColumn.new(name, Factory(name)) column.set_expand(i > 0) self.columnview.append_column(column) class SearchEntry(Gtk.SearchEntry): def __init__(self, filter): Gtk.SearchEntry.__init__(self, placeholder_text="Type To Filter", hexpand=True, margin_start=100, margin_end=100, margin_top=5, margin_bottom=5) self.popover = Gtk.Popover(child=Gtk.Label(), autohide=False) self.popover.set_parent(self) self.connect("search-changed", self.on_search_changed, filter) def on_search_changed(self, search, filter): try: filter.set_regex(search.get_text()) search.remove_css_class("warning") self.popover.popdown() except re.error as e: search.add_css_class("warning") self.popover.get_child().set_text(str(e)) self.popover.popup()