#!/usr/bin/python import gi gi.require_version("Gtk", "4.0") import bisect import pathlib 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" class Calendar(Gtk.Calendar): def __init__(self): Gtk.Calendar.__init__(self, show_day_names=True) self.connect("next-month", self.view_changed) self.connect("next-year", self.view_changed) self.connect("prev-month", self.view_changed) self.connect("prev-year", self.view_changed) self.view_changed(self) def get_month_directory(self): date = self.get_date() month = XFSTESTS / f"{date.get_year():04}" / f"{date.get_month():02}" return month if month.is_dir() else None def get_selected_day(self): if month := self.get_month_directory(): day = month / f"{self.get_date().get_day_of_month():02}" return day if day.is_dir() else None def view_changed(self, calendar): self.clear_marks() if month := self.get_month_directory(): for day in month.iterdir(): self.mark_day(int(day.stem)) self.emit("day-selected") class ServerRow(Gtk.TreeExpander): def __init__(self): Gtk.TreeExpander.__init__(self) self.set_child(Gtk.Label(xalign=0)) self.get_child().set_state_flags(Gtk.StateFlags.CHECKED, False) def set_item(self, item): self.set_list_row(item) self.get_child().set_text(item.get_item().get_name() if item else "") class ServerSorter(Gtk.Sorter): def do_compare(self, a, b): if a.get_name() < b.get_name(): return Gtk.Ordering.SMALLER elif a.get_name() > b.get_name(): return Gtk.Ordering.LARGER return Gtk.Ordering.EQUAL class ServerWindow(Gtk.ScrolledWindow): def __init__(self): Gtk.ScrolledWindow.__init__(self, vexpand=True, hscrollbar_policy=Gtk.PolicyType.NEVER) self.servers = Gtk.TreeListModel.new(Gtk.DirectoryList(), False, True, self.create_func) self.sorter = Gtk.TreeListRowSorter.new(ServerSorter()) self.sorted = Gtk.SortListModel.new(self.servers, self.sorter) self.selection = Gtk.SingleSelection.new(self.sorted) self.factory = Gtk.SignalListItemFactory() self.view = Gtk.ListView.new(self.selection, self.factory) self.selection.set_autoselect(False) self.factory.connect("setup", self.setup) self.factory.connect("bind", self.bind) self.factory.connect("unbind", self.unbind) self.factory.connect("teardown", self.teardown) self.selection.connect("selection-changed", self.selection_changed) self.set_child(self.view) def selection_changed(self, selection, position, n_items): treeitem = self.selection.get_selected_item() file = treeitem.get_item().get_attribute_object("standard::file") self.emit("test-changed", file if treeitem.get_depth() > 0 else None) def setup(self, factory, listitem): listitem.set_child(ServerRow()) def bind(self, factory, listitem): listitem.get_child().set_item(listitem.get_item()) listitem.set_selectable(listitem.get_item().get_depth() > 0) def unbind(self, factory, listitem): listitem.get_child().set_item(None) def teardown(self, factory, listitem): listitem.set_child(None) def create_func(self, item): file = item.get_attribute_object("standard::file") root = pathlib.Path(self.servers.get_model().get_file().get_path()) if pathlib.Path(file.get_path()).parent == root: return Gtk.DirectoryList.new(file=file) def set_day(self, day): file = Gio.File.new_for_path(str(day)) if day else None self.servers.get_model().set_file(file) @GObject.Signal(arg_types=(Gio.File,)) def test_changed(self, file): pass 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 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()): self.columns.append(file.stem) tree = xml.etree.ElementTree.parse(file) for elm in tree.getroot(): 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 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.selection = Gtk.NoSelection.new(self.results) 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_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) self.view.append_column(Gtk.ColumnViewColumn.new(name, TestResultsFactory(name))) class Window(Gtk.Window): def __init__(self): Gtk.Window.__init__(self, title="Xfstests Results") self.calendar = Calendar() self.servers = ServerWindow() self.xfstests = TestWindow() self.grid = Gtk.Grid() self.grid.attach(self.calendar, 0, 0, 1, 1) self.grid.attach(self.servers, 0, 1, 1, 1) self.grid.attach(self.xfstests, 1, 0, 1, 2) self.set_default_size(1000, 600) self.set_child(self.grid) self.calendar.connect("day-selected", self.date_changed) self.servers.connect("test-changed", self.test_changed) self.date_changed(self.calendar) def date_changed(self, calendar): self.servers.set_day(calendar.get_selected_day()) def test_changed(self, window, file): self.xfstests.set_tests(pathlib.Path(file.get_path())) 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()