258 lines
8.9 KiB
Python
258 lines
8.9 KiB
Python
|
#!/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("<span background='green'> Passed </span>")
|
||
|
case "Skipped": label.set_markup("<span background='orange'> Skipped </span>")
|
||
|
case "Failed": label.set_markup("<span background='red'> Failed </span>")
|
||
|
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()
|