Compare commits

...

8 Commits

Author SHA1 Message Date
Anna Schumaker 4d36a33840 report-xfstests.py: Improvements to the failed test output viewer
- Resize the window to double the width (and cut down on scrolling)
- Rename tabs
- Set default tab to the system-err field

Signed-off-by: Anna Schumaker <Anna.Schumaker@Netapp.com>
2022-02-07 15:24:47 -05:00
Anna Schumaker 55f3239edc report-xfstests.py: Move the test viewer into a new file
Also:
- Add some custom CSS to the widgets
- Add tooltips to each test result cell
- Add a popover with failing test output
- Improve filtering in place of the view switches

Signed-off-by: Anna Schumaker <Anna.Schumaker@Netapp.com>
2022-02-07 15:24:24 -05:00
Anna Schumaker 6f3c37e568 report-xfstests.py: Convert test properties into a Gtk.Stack
And use a Gtk.ColumnView to display the results instead of packing
everything into a Gtk.Grid

Signed-off-by: Anna Schumaker <Anna.Schumaker@Netapp.com>
2022-02-02 11:13:42 -05:00
Anna Schumaker b47955fe5a report-xfstests.py: Create an object that reads xfstests results
Splitting this out of the UI code should help clean things up.

Signed-off-by: Anna Schumaker <Anna.Schumaker@Netapp.com>
2022-02-01 15:59:50 -05:00
Anna Schumaker f1b6f42009 report-xfstests: Split out the test chooser into a new file
And add support for tests against tagged kernels

Signed-off-by: Anna Schumaker <Anna.Schumaker@Netapp.com>
2022-02-01 15:21:55 -05:00
Anna Schumaker 51f50a54b4 xfstests: Check if the current commit is a git tag
And create a link to the test results in a specific spot if it is so we
can eventually browse results by tag.

Signed-off-by: Anna Schumaker <Anna.Schumaker@Netapp.com>
2022-01-31 15:02:30 -05:00
Anna Schumaker e2c8b731d7 report-xfstests.py: Show test properties
I put these in a notebook with tabs based on how the server is mounted
(tcp-3, rdma-4.2, ...) so we can see extra test options that xfstests is
running with

Signed-off-by: Anna Schumaker <Anna.Schumaker@Netapp.com>
2022-01-28 16:29:21 -05:00
Anna Schumaker 58b9083908 xfstests.zsh: Don't create results directory until we have results
Otherwise the empty directory will show up in the results tool as an
option, which could leade to gtk errors when trying to populate the
results table.

Signed-off-by: Anna Schumaker <Anna.Schumaker@Netapp.com>
2022-01-28 16:26:27 -05:00
10 changed files with 630 additions and 340 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
__pycache__
*.pyc

View File

@ -2,4 +2,10 @@
set -eo pipefail
let jobs=$(nproc)-2
if [ -f .git/config ] && [ -f .git/HEAD ]; then
REV=$(git rev-parse --short HEAD)$(git diff --quiet || echo "-dirty")
scripts/config --set-str CONFIG_LOCALVERSION "-g$REV"
fi
make -j$jobs $* | python /home/anna/bin/colors/make.py

View File

@ -1,355 +1,27 @@
#!/usr/bin/python
import gi
gi.require_version("Gtk", "4.0")
import bisect
import pathlib
import re
import reporter
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
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()):
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 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.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)
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.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.calendar, 0, 0, 2, 1)
self.left.attach(self.servers, 0, 1, 2, 1)
self.left.attach(self.passing, 0, 2, 1, 1)
self.left.attach(self.lpass, 1, 2, 1, 1)
self.left.attach(self.skipped, 0, 3, 1, 1)
self.left.attach(self.lskip, 1, 3, 1, 1)
self.right = Gtk.Box.new(Gtk.Orientation.VERTICAL, 5)
self.right.append(self.search)
self.right.append(self.xfstests)
self.chooser = reporter.TestChooser()
self.testview = reporter.TestViewer()
self.child = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 5)
self.child.append(self.left)
self.child.append(self.right)
self.child.append(self.chooser)
self.child.append(self.testview)
self.lskip.set_xalign(0)
self.lpass.set_xalign(0)
self.set_default_size(1000, 600)
self.set_default_size(1300, 800)
self.set_child(self.child)
self.calendar.connect("day-selected", self.date_changed)
self.servers.connect("test-changed", 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)
self.date_changed(self.calendar)
def date_changed(self, calendar):
self.servers.set_day(calendar.get_selected_day())
self.chooser.connect("test-selected", self.test_changed)
if len(sys.argv) > 1:
self.testview.set_test_result(reporter.testchooser.Path(pathlib.Path(sys.argv[1])))
def test_changed(self, window, file):
self.xfstests.set_tests(pathlib.Path(file.get_path()))
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))
self.testview.set_test_result(file)
class Application(Gtk.Application):

47
reporter/__init__.py Normal file
View File

@ -0,0 +1,47 @@
#!/usr/bin/python
from . import common
from . import testchooser
from . import testproperties
from . import testresults
from . import testviewer
from gi.repository import GObject
from gi.repository import Gtk
SizeGroup = Gtk.SizeGroup.new(Gtk.SizeGroupMode.VERTICAL)
class TestChooser(Gtk.Box):
def __init__(self):
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
self.stack = testchooser.Stack()
self.switcher = Gtk.StackSwitcher(stack=self.stack)
SizeGroup.add_widget(self.switcher)
self.append(self.switcher)
self.append(self.stack)
self.stack.connect("test-selected", self.on_test_selected)
def on_test_selected(self, stack, path):
self.emit("test-selected", path)
@GObject.Signal(arg_types=(testchooser.Path,))
def test_selected(self, path): pass
class TestViewer(Gtk.Box):
def __init__(self):
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
self.stack = testproperties.Stack()
self.viewer = testviewer.Viewer()
self.switcher = Gtk.StackSwitcher(stack=self.stack, halign=Gtk.Align.CENTER)
self.append(self.switcher)
self.append(self.stack)
self.append(self.viewer)
self.append(testviewer.SearchEntry(self.viewer.get_filter()))
SizeGroup.add_widget(self.switcher)
def set_test_result(self, file):
self.stack.clear()
self.viewer.clear()
if file and file.get_is_test_result():
results = testresults.TestResults(file.path)
self.stack.show_properties(results)
self.viewer.show_results(results)

14
reporter/common.py Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/python
import gi
import pathlib
import xdg.BaseDirectory
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk
XFSTESTS_BASE = pathlib.Path(xdg.BaseDirectory.xdg_data_home) / "xfstests"
XFSTESTS_DATE = XFSTESTS_BASE / "date"
XFSTESTS_TAGS = XFSTESTS_BASE / "tags"
SizeGroup = Gtk.SizeGroup.new(Gtk.SizeGroupMode.VERTICAL)

163
reporter/testchooser.py Normal file
View File

@ -0,0 +1,163 @@
#!/usr/bin/python
import pathlib
from . import common
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Gio
class Path(GObject.GObject):
def __init__(self, path):
GObject.GObject.__init__(self)
self.path = path
def __lt__(self, rhs): return self.path < rhs.path
def get_is_test_result(self):
return False not in [ f.is_file() for f in self.path.iterdir() ]
def get_name(self): return self.path.name
def get_model(self):
if not self.get_is_test_result():
return DirectoryModel(self.path)
class DirectoryModel(GObject.GObject, Gio.ListModel):
def __init__(self, path=None):
GObject.GObject.__init__(self)
self.children = [ ]
self.set_path(path)
def do_get_item_type(self): return GObject.TYPE_PYOBJECT
def do_get_n_items(self): return len(self.children)
def do_get_item(self, n): return self.children[n]
def set_path(self, path):
rm = len(self.children)
self.children.clear()
self.path = path if path and path.is_dir() else None
if self.path:
self.children = sorted([ Path(c) for c in path.iterdir() ])
self.emit("items-changed", 0, rm, len(self.children))
class DirectoryWindow(Gtk.ScrolledWindow):
def __init__(self, path=None, autoexpand=True):
Gtk.ScrolledWindow.__init__(self, vexpand=True,
hscrollbar_policy=Gtk.PolicyType.NEVER)
self.dirtree = Gtk.TreeListModel.new(root=DirectoryModel(path),
passthrough=False,
autoexpand=autoexpand,
create_func=self.create_func)
self.selection = Gtk.SingleSelection.new(self.dirtree)
self.selection.set_autoselect(False)
self.selection.set_can_unselect(True)
self.selection.connect("selection-changed", self.selection_changed)
self.factory = Gtk.SignalListItemFactory()
self.factory.connect("setup", self.on_setup)
self.factory.connect("bind", self.on_bind)
self.factory.connect("unbind", self.on_unbind)
self.factory.connect("teardown", self.on_teardown)
self.listview = Gtk.ListView.new(self.selection, self.factory)
self.set_child(self.listview)
def create_func(self, path):
return path.get_model()
def on_setup(self, factory, listitem):
listitem.set_child(Gtk.TreeExpander(child=Gtk.Label(xalign=0)))
def on_bind(self, factory, listitem):
treerow = listitem.get_item()
filepath = treerow.get_item()
expander = listitem.get_child()
expander.set_list_row(treerow)
expander.get_child().set_text(filepath.get_name())
listitem.set_selectable(filepath.get_is_test_result())
def on_unbind(self, factory, listitem):
expander = listitem.get_child()
expander.set_list_row(None)
expander.get_child().set_text("")
def on_teardown(self, factory, treeitem):
treeitem.set_child(None)
def set_directory(self, path):
self.dirtree.get_model().set_path(path)
def selection_changed(self, selection, position, n_items):
treeitem = self.selection.get_selected_item()
self.emit("test-selected", treeitem.get_item() if treeitem else None)
def select_none(self):
self.selection.unselect_item(self.selection.get_selected())
@GObject.Signal(arg_types=(Path,))
def test_selected(self, filepath): pass
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)
common.SizeGroup.add_widget(self)
self.view_changed(self)
def get_month_directory(self):
date = self.get_date()
month = common.XFSTESTS_DATE / 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 DatePage(Gtk.Box):
def __init__(self):
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=5)
self.calendar = Calendar()
self.dirwin = DirectoryWindow(self.calendar.get_selected_day())
self.append(self.calendar)
self.append(self.dirwin)
self.get_first_child().connect("day-selected", self.on_day_selected)
def on_day_selected(self, calendar):
self.dirwin.set_directory(self.calendar.get_selected_day())
def select_none(self):
self.dirwin.select_none()
class Stack(Gtk.Stack):
def __init__(self):
Gtk.Stack.__init__(self, transition_type=Gtk.StackTransitionType.OVER_LEFT_RIGHT)
self.date = DatePage()
self.tags = DirectoryWindow(common.XFSTESTS_TAGS, autoexpand=False)
self.add_titled(self.date, "Date", "Date")
self.add_titled(self.tags, "Tags", "Tags")
self.date.dirwin.connect("test-selected", self.on_test_selected)
self.tags.connect("test-selected", self.on_test_selected)
self.connect("notify::visible-child", self.on_page_changed)
def on_test_selected(self, widget, test):
self.emit("test-selected", test)
def on_page_changed(self, stack, param):
stack.get_visible_child().select_none()
self.emit("test-selected", None)
@GObject.Signal(arg_types=(Path,))
def test_selected(self, path): pass

View File

@ -0,0 +1,90 @@
#!/usr/bin/python
from . import common
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
PROPERTIES = [ "PLATFORM", "TIMESTAMP", "FSTYP", "MOUNT_OPTIONS", "CHECK_OPTIONS",
"TEST_DIR", "TEST_DEV", "SCRATCH_DEV", "SCRATCH_MNT" ] #, "RESULTS" ]
class Property(GObject.GObject):
def __init__(self, key, value):
GObject.GObject.__init__(self)
self.key = key
self.value = value
class Results(Property):
def __init__(self, properties):
total = properties["TESTS"]
time = properties["TIME"]
failed = properties["FAILURES"]
skipped = properties["SKIPPED"]
passed = int(total) - (int(skipped) + int(failed))
Property.__init__(self, "RESULTS",
f"Ran {total} tests in {time} seconds: " \
f"{passed} passed, {failed} failed, {skipped} skipped")
class Model(GObject.GObject, Gio.ListModel):
def __init__(self, properties):
GObject.GObject.__init__(self)
self.properties = [ Property(p, properties[p]) for p in PROPERTIES ]
self.properties.append(Results(properties))
def do_get_item_type(self): return GObject.TYPE_PYOBJECT
def do_get_n_items(self): return len(self.properties)
def do_get_item(self, i): return self.properties[i]
class Factory(Gtk.SignalListItemFactory):
def __init__(self, column):
Gtk.SignalListItemFactory.__init__(self)
self.column = column
self.connect("setup", self.on_setup)
self.connect("bind", self.on_bind)
self.connect("unbind", self.on_unbind)
self.connect("teardown", self.on_teardown)
def on_setup(self, factory, listitem):
listitem.set_child(Gtk.Label(xalign=0))
def on_bind(self, factory, listitem):
label = listitem.get_child()
match self.column:
case "Property": label.set_text(listitem.get_item().key)
case "Value": label.set_text(listitem.get_item().value)
case _: label.set_text("=")
def on_unbind(self, factory, listitem):
listitem.get_child().set_text("")
def on_teardown(self, factory, listitem):
listitem.set_child(None)
class View(Gtk.ColumnView):
def __init__(self, properties):
self.selection = Gtk.NoSelection.new(Model(properties))
Gtk.ColumnView.__init__(self, model=self.selection)
self.add_css_class("data-table")
for title in [ "Property", "=", "Value" ]:
self.append_column(Gtk.ColumnViewColumn.new(title, Factory(title)))
class Stack(Gtk.Stack):
def __init__(self):
Gtk.Stack.__init__(self, transition_type=Gtk.StackTransitionType.OVER_LEFT_RIGHT)
common.SizeGroup.add_widget(self)
def clear(self):
pages = self.get_pages()
children = [ pages.get_item(i).get_child() for i in range(pages.get_n_items()) ]
for child in children:
self.remove(child)
def show_properties(self, results):
for version in results.versions:
window = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER,
child=View(results.properties[version]))
self.add_titled(window, version, version)

77
reporter/testresults.py Normal file
View File

@ -0,0 +1,77 @@
#!/usr/bin/python
from gi.repository import GObject
from gi.repository import Gtk
import html
import xml.etree.ElementTree
class PassingTest:
def __init__(self, elm):
self.time = elm.attrib["time"]
def timestr(self):
(m, s) = divmod(int(self.time), 60)
res = [ f"{s} second{'' if s == 1 else 's'}" ]
if m > 0: res.insert(0, f"{m} minute{'' if m == 1 else 's'}")
return ' and '.join(res)
class SkippedTest:
def __init__(self, elm):
self.time = elm.attrib["time"]
self.message = elm[0].attrib["message"]
class FailedTest:
def __init__(self, elm):
self.time = elm.attrib["time"]
self.system_out = Gtk.TextBuffer()
self.system_err = Gtk.TextBuffer()
for e in elm:
match e.tag:
case "failure": self.message = e.attrib["message"]
case "system-out": self.system_out.set_text(html.unescape(e.text))
case "system-err": self.system_err.set_text(html.unescape(e.text))
class TestCase(GObject.GObject):
def __init__(self, name):
GObject.GObject.__init__(self)
self.name = name
self.versions = dict()
def __getitem__(self, key): return self.versions.get(key, None)
def __setitem__(self, key, value): self.versions[key] = value
class TestResults:
def __init__(self, testdir):
self.versions = [ ]
self.tests = dict()
self.properties = dict()
for file in sorted(testdir.iterdir()):
self.versions.append(file.stem)
root = xml.etree.ElementTree.parse(file).getroot()
for prop in root.attrib.keys():
self.set_property(prop, file.stem, root.attrib[prop])
for elm in root:
if elm.tag == "properties":
for prop in elm:
self.set_property(prop.attrib["name"], file.stem,
prop.attrib["value"])
elif elm.tag == "testcase":
if len(elm) == 0:
result = PassingTest(elm)
elif elm[0].tag == "skipped":
result = SkippedTest(elm)
elif elm[0].tag == "failure":
result = FailedTest(elm)
self.add_testcase(elm.attrib["name"], file.stem, result)
def set_property(self, name, vers, value):
self.properties.setdefault(vers, dict())[name.upper()] = value
def add_testcase(self, name, vers, result):
self.tests.setdefault(name, TestCase(name))[vers] = result

209
reporter/testviewer.py Normal file
View File

@ -0,0 +1,209 @@
#!/usr/bin/python
import itertools
import re
from . import testresults
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
CSS = """
@define-color darkorange #cc7000;
@define-color darkred #8b0000;
label.passing-test {
background-color: green;
padding: 4px;
}
label.skipped-test {
background-color: darkorange;
padding: 4px;
}
menubutton.failed-test>button {
background: darkred;
border-color: darkred;
border-radius: 0px;
padding: 0px;
}
"""
def markup_tooltip(status, color, message):
return f"<span background='{color}'> Test {status} </span> {message}"
class Label(Gtk.Label):
def __init__(self, text):
Gtk.Label.__init__(self, xalign=0)
self.set_text(text)
class PassingTest(Gtk.Label):
def __init__(self, test):
Gtk.Label.__init__(self)
self.add_css_class("passing-test")
self.set_text(f"{test.time} seconds")
self.set_tooltip_markup(markup_tooltip("Passed", "green",
f"in {test.timestr()}"))
class SkippedTest(Gtk.Label):
def __init__(self, test):
Gtk.Label.__init__(self)
self.add_css_class("skipped-test")
self.set_text("Skipped")
self.set_tooltip_markup(markup_tooltip("Skipped", "#cc7000", test.message))
class TestOutput(Gtk.Box):
def __init__(self, sysout, syserr):
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
self.add_css_class("popover")
self.stack = Gtk.Stack(vexpand=True, transition_type=Gtk.StackTransitionType.OVER_LEFT_RIGHT)
self.prepend(self.stack)
self.prepend(Gtk.StackSwitcher(stack=self.stack))
self.add_page("system-out", sysout, visible=False)
self.add_page("system-err", syserr, visible=True)
self.set_size_request(900, 500)
def add_page(self, name, buffer, visible):
view = Gtk.TextView(monospace=True)
view.set_buffer(buffer)
self.stack.add_titled(Gtk.ScrolledWindow(child=view), name, name)
if visible:
self.stack.set_visible_child_name(name)
class FailedTest(Gtk.MenuButton):
def __init__(self, test):
Gtk.MenuButton.__init__(self, label="Failed")
self.set_tooltip_markup(markup_tooltip("Failed", "#8b0000", test.message))
self.set_popover(Gtk.Popover(child=TestOutput(test.system_out, test.system_err)))
self.add_css_class("failed-test")
class MissingTest(Gtk.Label):
def __init__(self, test):
Gtk.Label.__init__(self, sensitive=False)
self.set_name("missing-test")
self.set_text("Missing")
self.set_tooltip_text("Test Data Is Missing")
class Model(GObject.GObject, Gio.ListModel):
def __init__(self, results):
GObject.GObject.__init__(self)
self.results = results
self.columns = [ "Testcase" ] + self.results.versions
self.tests = sorted(results.tests.keys())
def get_columns(self): return self.columns
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, i): return self.results.tests[self.tests[i]]
class Filter(Gtk.Filter):
def __init__(self):
Gtk.Filter.__init__(self)
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 do_match(self, testcase):
search = [ f"name={testcase.name}" ]
for key in sorted(testcase.versions.keys()):
keys = [ key ] + key.split("-")
match type(testcase[key]):
case testresults.PassingTest: res = [ "passing", "passed" ]
case testresults.SkippedTest: res = [ "skipped" ]
case testresults.FailedTest: res = [ "failed", "failing" ]
case _: res = [ "missing" ]
for k in keys:
for r in res:
search.append(f"{k}={r}")
return self.regex.search(" ".join(search)) != None
class Factory(Gtk.SignalListItemFactory):
def __init__(self, column):
Gtk.SignalListItemFactory.__init__(self)
self.column = column
self.connect("bind", self.on_bind)
self.connect("unbind", self.on_unbind)
def on_bind(self, factory, listitem):
testcase = listitem.get_item()
if self.column == "Testcase":
child = Label(testcase.name)
else:
result = testcase[self.column]
match type(result):
case testresults.PassingTest: child = PassingTest(result)
case testresults.SkippedTest: child = SkippedTest(result)
case testresults.FailedTest: child = FailedTest(result)
case _: child = child = MissingTest(result)
listitem.set_child(child)
def on_unbind(self, factory, listitem):
listitem.set_child(None)
class Viewer(Gtk.ScrolledWindow):
def __init__(self):
Gtk.ScrolledWindow.__init__(self, hscrollbar_policy=Gtk.PolicyType.NEVER,
hexpand=True, vexpand=True, margin_top=5)
self.provider = Gtk.CssProvider()
self.filter = Gtk.FilterListModel.new(filter=Filter())
self.selection = Gtk.NoSelection.new(self.filter)
self.columnview = Gtk.ColumnView.new(model=self.selection)
self.columnview.add_css_class("data-table")
self.provider.load_from_data(CSS.encode())
self.get_style_context().add_provider_for_display(self.get_display(), self.provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
self.set_child(self.columnview)
def get_filter(self):
return self.filter.get_filter()
def clear(self):
columns = self.columnview.get_columns()
columns = [ columns.get_item(i) for i in range(columns.get_n_items()) ]
for col in columns:
self.columnview.remove_column(col)
self.filter.set_model(None)
def show_results(self, results):
if results:
self.filter.set_model(Model(results))
for i, name in enumerate(self.filter.get_model().get_columns()):
column = Gtk.ColumnViewColumn.new(name, Factory(name))
column.set_expand(i > 0)
self.columnview.append_column(column)
class SearchEntry(Gtk.SearchEntry):
def __init__(self, filter):
Gtk.SearchEntry.__init__(self, placeholder_text="Type To Filter",
hexpand=True, margin_start=100, margin_end=100,
margin_top=5, margin_bottom=5)
self.popover = Gtk.Popover(child=Gtk.Label(), autohide=False)
self.popover.set_parent(self)
self.connect("search-changed", self.on_search_changed, filter)
def on_search_changed(self, search, filter):
try:
filter.set_regex(search.get_text())
search.remove_css_class("warning")
self.popover.popdown()
except re.error as e:
search.add_css_class("warning")
self.popover.get_child().set_text(str(e))
self.popover.popup()

View File

@ -23,10 +23,16 @@ COLOR=$BIN/colors/xfstests.py
RUN_XFSTESTS="sudo run-xfstests.zsh"
OPTIONS="sec=sys"
USER=$(whoami)
RESULT_BASE=$HOME/.local/share/xfstests/
DATE=$(date +%Y-%m-%d-%H:%M:%S%z)
TODAY=$(date +%Y/%m/%d)
NOW=$(date +%H:%M:%S%z)
REMOTE_RESULTS=xfstests-dev/results/
RESULTS=$HOME/.local/share/xfstests/$TODAY/${SERVER[-1]}/$NOW
RESULTS=$RESULT_BASE/date/$TODAY/${SERVER[-1]}/$NOW
TAG=$(kernel-tag.zsh ${CLIENT[-1]})
if [ ! -z $TAG ]; then
TAGRES=$RESULT_BASE/tags/$TAG/${SERVER[-1]}
fi
#
# Prepare to test
@ -57,13 +63,13 @@ run_xfs_tests() {
--version $2 \
--opts $OPTIONS \
--user $USER $testargs" | python $COLOR $1 $2
mkdir -p $RESULTS
scp -q ${CLIENT[-1]}:$REMOTE_RESULTS/$1/$2/result.xml $RESULTS/$1-$2.xml
}
#
# Run tests
#
mkdir -p $RESULTS
for proto in $PROTO; do
for vers in $VERSION; do
case $vers in
@ -76,4 +82,8 @@ done
wait
if [ ! -z "$TAG" ]; then
mkdir -p $TAGRES
ln -s $RESULTS $TAGRES/$DATE
fi
optirun report-xfstests.py $RESULTS