330 lines
11 KiB
Python
Executable File
330 lines
11 KiB
Python
Executable File
#!/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 TestProperty():
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.prop = dict()
|
|
|
|
def __lt__(self, rhs):
|
|
return self.name.upper() < rhs.name.upper()
|
|
|
|
def __getitem__(self, key):
|
|
return self.prop[key]
|
|
|
|
def __setitem__(self, key, val):
|
|
self.prop[key] = val
|
|
|
|
|
|
class PropertyPage(Gtk.ScrolledWindow):
|
|
def __init__(self, name, results):
|
|
Gtk.ScrolledWindow.__init__(self, vscrollbar_policy=Gtk.PolicyType.NEVER)
|
|
self.set_child(Gtk.Grid())
|
|
|
|
res = { p.name : p[name] for p in results.properties }
|
|
passed = int(res['tests']) - (int(res['skipped']) + int(res['failures']))
|
|
res["RESULTS"] = f"Ran {res['tests']} tests in {res['time']} seconds: " \
|
|
f"{passed} passed, {res['failures']} failed, {res['skipped']} skipped"
|
|
|
|
fields = [ "PLATFORM", "timestamp", "FSTYP", "MOUNT_OPTIONS", "CHECK_OPTIONS",
|
|
"TEST_DIR", "TEST_DEV", "SCRATCH_DEV", "SCRATCH_MNT", "RESULTS" ]
|
|
for i, field in enumerate(fields):
|
|
key = Gtk.Label.new(field.upper() + " = ")
|
|
key.set_xalign(100)
|
|
val = Gtk.Label.new(res[field])
|
|
val.set_xalign(0)
|
|
self.get_child().get_child().attach(key, 0, i, 1, 1)
|
|
self.get_child().get_child().attach(val, 1, i, 1, 1)
|
|
|
|
|
|
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.properties = [ ]
|
|
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_property(self, name):
|
|
for prop in self.properties:
|
|
if prop.name == name:
|
|
return prop
|
|
self.properties.append(TestProperty(name))
|
|
return self.properties[-1]
|
|
|
|
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.properties.clear()
|
|
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 prop in root.attrib.keys():
|
|
self.find_property(prop)[file.stem] = root.attrib[prop]
|
|
|
|
for elm in root:
|
|
if elm.tag == "testcase":
|
|
testcase = self.find_testcase(elm.attrib["name"])
|
|
testcase[file.stem] = elm
|
|
elif elm.tag == "properties":
|
|
for prop in elm:
|
|
name = prop.attrib["name"]
|
|
value = prop.attrib["value"]
|
|
self.find_property(name)[file.stem] = value
|
|
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("<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.filter = Gtk.FilterListModel.new(self.results, TestResultsFilter())
|
|
self.selection = Gtk.NoSelection.new(self.filter)
|
|
self.view = Gtk.ColumnView.new(self.selection)
|
|
self.notebook = Gtk.Notebook()
|
|
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)
|
|
for n in range(self.notebook.get_n_pages()):
|
|
self.notebook.remove_page(0)
|
|
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)
|
|
if n > 0:
|
|
self.notebook.append_page(PropertyPage(name, self.results),
|
|
Gtk.Label.new(name))
|
|
|
|
|
|
class Window(Gtk.Window):
|
|
def __init__(self):
|
|
Gtk.Window.__init__(self, title="Xfstests Results")
|
|
self.chooser = reporter.TestChooser()
|
|
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.xfstests.notebook)
|
|
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)
|
|
|
|
reporter.common.SizeGroup.add_widget(self.xfstests.notebook)
|
|
|
|
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.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()
|