diff --git a/report-xfstests.py b/report-xfstests.py
new file mode 100755
index 0000000..d938f59
--- /dev/null
+++ b/report-xfstests.py
@@ -0,0 +1,257 @@
+#!/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()
diff --git a/run-xfstests.zsh b/run-xfstests.zsh
index 6e53e29..e483d1a 100755
--- a/run-xfstests.zsh
+++ b/run-xfstests.zsh
@@ -30,5 +30,5 @@ if [ ! -z "$*" ]; then
echo "XFSTESTS_ARGS -- $*"
fi
-#./check -nfs -r -R xunit $*
-./check -nfs -R xunit $*
+./check -nfs -r -R xunit $*
+#./check -nfs -R xunit $*
diff --git a/xfstests.zsh b/xfstests.zsh
index e21ba07..b562fbf 100755
--- a/xfstests.zsh
+++ b/xfstests.zsh
@@ -24,9 +24,9 @@ RUN_XFSTESTS="sudo run-xfstests.zsh"
OPTIONS="sec=sys"
USER=$(whoami)
TODAY=$(date +%Y/%m/%d)
-NOW=$(date +%H:%M:%S)
+NOW=$(date +%H:%M:%S%z)
REMOTE_RESULTS=xfstests-dev/results/
-RESULTS=$HOME/.local/share/xfstests/${SERVER[-1]}/$TODAY
+RESULTS=$HOME/.local/share/xfstests/$TODAY/${SERVER[-1]}/$NOW
#
# Prepare to test
@@ -57,7 +57,7 @@ run_xfs_tests() {
--version $2 \
--opts $OPTIONS \
--user $USER $testargs" | python $COLOR $1 $2
- scp -q ${CLIENT[-1]}:$REMOTE_RESULTS/$1/$2/result.xml $RESULTS/$NOW-$1-$2.xml
+ scp -q ${CLIENT[-1]}:$REMOTE_RESULTS/$1/$2/result.xml $RESULTS/$1-$2.xml
}
#
@@ -75,3 +75,5 @@ for proto in $PROTO; do
done
wait
+
+optirun report-xfstests.py $RESULTS