#!/usr/bin/python import gi gi.require_version("Gtk", "4.0") import bisect import pathlib import reporter import re import sys import xdg.BaseDirectory import xml.etree.ElementTree from gi.repository import Gtk from gi.repository import GObject from gi.repository import Gio XFSTESTS = pathlib.Path(xdg.BaseDirectory.xdg_data_home) / "xfstests" / "date" class TestCase(GObject.GObject): def __init__(self, name): GObject.GObject.__init__(self) self.name = name self.tests = dict() def __getitem__(self, key): if key == "Test Name": return self.name match len(self.tests[key]) if key in self.tests else None: case None: return "Missing" case 0: return "Passed" case 1: return "Skipped" case _: return "Failed" def __setitem__(self, key, val): self.tests[key] = val def __str__(self): return self.name def __lt__(self, rhs): return self.name < rhs.name def passed(self): return False not in [ len(elm) == 0 for elm in self.tests.values() ] def failed(self): return 3 in [ len(elm) for elm in self.tests.values() ] def skipped(self): return False not in [ len(elm) == 1 for elm in self.tests.values() ] class TestResultsModel(GObject.GObject, Gio.ListModel): def __init__(self): GObject.GObject.__init__(self) self.tests = [ ] self.columns = [ "Test Name" ] 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, n): return self.tests[n] def get_n_columns(self): return len(self.columns) def get_column_name(self, n): return self.columns[n] def find_testcase(self, name): i = bisect.bisect(self.tests, name, key=str) if i > 0 and self.tests[i-1].name == name: return self.tests[i-1] self.tests.insert(i, TestCase(name)) return self.tests[i] def set_tests(self, path): rm = len(self.tests) self.columns = [ "Test Name" ] self.tests.clear() for file in sorted(path.iterdir() if path else []): self.columns.append(file.stem) root = xml.etree.ElementTree.parse(file).getroot() for elm in root: if elm.tag == "testcase": testcase = self.find_testcase(elm.attrib["name"]) testcase[file.stem] = elm self.emit("items-changed", 0, rm, len(self.tests)) class TestResultsFilter(Gtk.Filter): def __init__(self): Gtk.Filter.__init__(self) self.show_passing = True self.show_skipped = True 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 set_show_passing(self, val): self.show_passing = val self.set_show_changed(self.show_passing) def set_show_skipped(self, val): self.show_skipped = val self.set_show_changed(self.show_skipped) def set_show_changed(self, newval): match newval: case True: self.changed(Gtk.FilterChange.LESS_STRICT) case False: self.changed(Gtk.FilterChange.MORE_STRICT) def do_match(self, item): if not self.show_passing and item.passed(): return False if not self.show_skipped and item.skipped(): return False if not self.show_passing and not self.show_skipped and not item.failed(): return False return self.regex.search(item.name) != None class FilterPopover(Gtk.Popover): def __init__(self, parent): Gtk.Popover.__init__(self) self.set_child(Gtk.Label()) self.set_autohide(False) self.set_parent(parent) def popup(self, text): self.get_child().set_text(text) super().popup() class TestResultsFactory(Gtk.SignalListItemFactory): def __init__(self, column): Gtk.SignalListItemFactory.__init__(self) self.column = column self.connect("setup", self.setup) self.connect("bind", self.bind) self.connect("unbind", self.unbind) self.connect("teardown", self.teardown) def setup(self, factory, listitem): listitem.set_child(Gtk.Label(xalign=0)) def bind(self, factory, listitem): label = listitem.get_child() text = listitem.get_item()[self.column] match text: case "Passed": label.set_markup(" Passed ") case "Skipped": label.set_markup(" Skipped ") case "Failed": label.set_markup(" Failed ") case _: label.set_text(" " + text) def unbind(self, factory, listitem): listitem.get_child().set_text("") def teardown(self, factory, listitem): listitem.set_child(None) class TestWindow(Gtk.ScrolledWindow): def __init__(self): Gtk.ScrolledWindow.__init__(self, vexpand=True, hexpand=True, hscrollbar_policy=Gtk.PolicyType.NEVER) self.results = TestResultsModel() self.filter = Gtk.FilterListModel.new(self.results, TestResultsFilter()) self.selection = Gtk.NoSelection.new(self.filter) self.view = Gtk.ColumnView.new(self.selection) self.set_child(self.view) if len(sys.argv) > 1: self.set_tests(pathlib.Path(sys.argv[1])) def set_show_passing(self, show): self.filter.get_filter().set_show_passing(show) def set_show_skipped(self, show): self.filter.get_filter().set_show_skipped(show) def set_filter_regex(self, text): self.filter.get_filter().set_regex(text) def set_tests(self, path): for column in [ c for c in self.view.get_columns() ]: self.view.remove_column(column) self.results.set_tests(path) for n in range(self.results.get_n_columns()): name = self.results.get_column_name(n) col = Gtk.ColumnViewColumn.new(name, TestResultsFactory(name)) col.set_expand(n > 0) self.view.append_column(col) class Window(Gtk.Window): def __init__(self): Gtk.Window.__init__(self, title="Xfstests Results") self.chooser = reporter.TestChooser() self.testview = reporter.TestView() self.passing = Gtk.Switch(halign=Gtk.Align.CENTER, active=True) self.lpass = Gtk.Label.new("Show Passing Tests") self.skipped = Gtk.Switch(halign=Gtk.Align.CENTER, active=True) self.lskip = Gtk.Label.new("Show Skipped Tests") self.search = Gtk.SearchEntry(placeholder_text="Type To Filter Tests") self.popover = FilterPopover(parent=self.search) self.xfstests = TestWindow() self.left = Gtk.Grid(row_spacing=5) self.left.attach(self.chooser, 0, 0, 2, 1) self.left.attach(self.passing, 0, 1, 1, 1) self.left.attach(self.lpass, 1, 1, 1, 1) self.left.attach(self.skipped, 0, 2, 1, 1) self.left.attach(self.lskip, 1, 2, 1, 1) self.right = Gtk.Box.new(Gtk.Orientation.VERTICAL, 5) self.right.append(self.testview) self.right.append(self.search) self.right.append(self.xfstests) self.child = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 5) self.child.append(self.left) self.child.append(self.right) self.lskip.set_xalign(0) self.lpass.set_xalign(0) self.set_default_size(1100, 750) self.set_child(self.child) self.chooser.connect("test-selected", self.test_changed) self.passing.connect("notify::active", self.show_passing) self.skipped.connect("notify::active", self.show_skipped) self.search.connect("search-changed", self.search_changed) def test_changed(self, window, file): self.testview.set_test_result(file) self.xfstests.set_tests(file.path if file else None) def show_passing(self, passing, param): self.xfstests.set_show_passing(passing.get_active()) def show_skipped(self, skipped, param): self.xfstests.set_show_skipped(skipped.get_active()) def search_changed(self, search): try: self.xfstests.set_filter_regex(search.get_text()) self.search.remove_css_class("warning") self.popover.popdown() except re.error as e: self.search.add_css_class("warning") self.popover.popup(str(e)) class Application(Gtk.Application): def __init__(self, *args, **kwargs): Gtk.Application.__init__(self, *args, application_id="org.gtk.report-xfstests", **kwargs) def do_startup(self): Gtk.Application.do_startup(self) self.add_window(Window()) def do_activate(self): for window in self.get_windows(): window.present() if __name__ == "__main__": Application().run()