gtk: Put the TestResultList behind a TestCaseFilter

And control the filter using the newly-created FilterButtons class. This
lets us hide completely skipped tests by default, since those are mostly
noise. I also add some custom icons used by the buttons to indicate
passed, failed, or skipped tests.

Signed-off-by: Anna Schumaker <anna@nowheycreamery.com>
This commit is contained in:
Anna Schumaker 2023-08-02 12:12:40 -04:00
parent 37079ca7f5
commit 8a54cb5d98
13 changed files with 199 additions and 8 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
*__pycache__*
xfstestsdb/gtk/icons/xfstestsdb.gresource

View File

@ -3,6 +3,9 @@
export PREFIX = /usr/local
export XFSTESTSDB_BIN = ${PREFIX}/bin
export XFSTESTSDB_LIB = ${PREFIX}/lib/xfstestsdb
export GTK_DIR = xfstestsdb/gtk/icons
all: xfstestsdb.gresource flake8
clean:
find . -type d -name __pycache__ -exec rm -r {} \+
@ -11,8 +14,12 @@ clean:
flake8:
flake8
.PHONY: xfstestsdb.gresource
xfstestsdb.gresource:
glib-compile-resources --sourcedir=$(GTK_DIR) $(GTK_DIR)/xfstestsdb.gresource.xml
.PHONY: install
install:
install: xfstestsdb.gresource
find ./xfstestsdb -type f -not -path "*/__pycache__/*" \
-exec install -v -C -D -m 755 "{}" "$(XFSTESTSDB_LIB)/{}" \;
install -C -v -m 644 xfstestsdb.py $(XFSTESTSDB_LIB)/xfstestsdb.py
@ -24,7 +31,7 @@ pytest:
pytest
.PHONY: tests
tests: pytest flake8
tests: xfstestsdb.gresource pytest flake8
.PHONY: uninstall
uninstall:

View File

@ -40,3 +40,15 @@ class TestGSetup(unittest.TestCase):
mock_add.assert_called_with(mock_get_default.return_value,
xfstestsdb.gtk.gsetup.CSS_PROVIDER,
xfstestsdb.gtk.gsetup.CSS_PRIORITY)
@unittest.mock.patch("gi.repository.Gio.resources_register")
def test_resources(self, mock_register: unittest.mock.Mock):
"""Test that icon resources have been added to the app."""
gtk_init_py = pathlib.Path(xfstestsdb.gtk.__file__)
resources = gtk_init_py.parent / "icons" / "xfstestsdb.gresource"
self.assertEqual(xfstestsdb.gtk.gsetup.RESOURCE_FILE, resources)
self.assertEqual(xfstestsdb.gtk.gsetup.RESOURCE_PATH,
"/com/nowheycreamery/xfstestsdb")
self.assertIsInstance(xfstestsdb.gtk.gsetup.RESOURCE,
gi.repository.Gio.Resource)

View File

@ -6,6 +6,63 @@ import xfstestsdb.gtk.view
from gi.repository import Gtk
class TestFilterButtons(unittest.TestCase):
"""Test case for our TestCaseView FilterButtons."""
def setUp(self):
"""Set up common variables."""
self.buttons = xfstestsdb.gtk.view.FilterButtons()
def test_init(self):
"""Check that the buttons were created correctly."""
self.assertIsInstance(self.buttons, Gtk.Box)
self.assertTrue(self.buttons.has_css_class("linked"))
def test_passed_button(self):
"""Test the 'passed' button."""
self.assertIsInstance(self.buttons._passed,
xfstestsdb.gtk.button.StatusToggle)
self.assertEqual(self.buttons.get_first_child(), self.buttons._passed)
self.assertEqual(self.buttons._passed.props.icon_name, "test-pass")
self.assertTrue(self.buttons._passed.has_css_class("passed"))
self.assertFalse(self.buttons._passed.props.active)
self.assertFalse(self.buttons.passed)
self.buttons._passed.props.active = True
self.assertTrue(self.buttons.passed)
def test_skipped_button(self):
"""Test the 'skipped' button."""
self.assertIsInstance(self.buttons._skipped,
xfstestsdb.gtk.button.StatusToggle)
self.assertEqual(self.buttons._passed.get_next_sibling(),
self.buttons._skipped)
self.assertEqual(self.buttons._skipped.props.icon_name, "test-skip")
self.assertTrue(self.buttons._skipped.has_css_class("skipped"))
self.assertFalse(self.buttons._skipped.props.active)
self.assertFalse(self.buttons.skipped)
self.buttons._skipped.props.active = True
self.assertTrue(self.buttons.skipped)
def test_failure_button(self):
"""Test the 'failure' button."""
self.assertIsInstance(self.buttons._failure,
xfstestsdb.gtk.button.StatusToggle)
self.assertEqual(self.buttons._skipped.get_next_sibling(),
self.buttons._failure)
self.assertEqual(self.buttons._failure.props.icon_name, "test-fail")
self.assertTrue(self.buttons._failure.has_css_class("failure"))
self.assertTrue(self.buttons._failure.props.active)
self.assertTrue(self.buttons.failure)
self.buttons._failure.props.active = False
self.assertFalse(self.buttons.failure)
class TestTestCaseView(unittest.TestCase):
"""Tests the TestCaseView."""
@ -39,6 +96,31 @@ class TestTestCaseView(unittest.TestCase):
self.assertTrue(self.view.props.child.get_hexpand())
self.assertTrue(self.view.props.child.get_vexpand())
def test_filter(self):
"""Test that we set up the Gtk.FilterModel and Buttons correctly."""
self.assertIsInstance(self.view._filtermodel, Gtk.FilterListModel)
self.assertIsInstance(self.view._testfilter,
xfstestsdb.gtk.model.TestCaseFilter)
self.assertIsInstance(self.view.filterbuttons,
xfstestsdb.gtk.view.FilterButtons)
self.assertEqual(self.view.props.child.get_model().get_model(),
self.view._filtermodel)
self.assertEqual(self.view._filtermodel.props.filter,
self.view._testfilter)
self.assertFalse(self.view._testfilter.passed)
self.view.filterbuttons.passed = True
self.assertTrue(self.view._testfilter.passed)
self.assertFalse(self.view._testfilter.skipped)
self.view.filterbuttons.skipped = True
self.assertTrue(self.view._testfilter.skipped)
self.assertTrue(self.view._testfilter.failure)
self.view.filterbuttons.failure = False
self.assertFalse(self.view._testfilter.failure)
def test_testcase_column(self):
"""Test that we set up the 'testcase' column correctly."""
self.assertIsInstance(self.view._testcase, Gtk.ColumnViewColumn)
@ -51,8 +133,7 @@ class TestTestCaseView(unittest.TestCase):
def test_model(self):
"""Test setting the model property."""
self.view.model = self.model
self.assertEqual(self.view.props.child.get_model().get_model(),
self.model)
self.assertEqual(self.view._filtermodel.props.model, self.model)
columns = self.view.props.child.get_columns()
self.assertEqual(len(columns), 3)
@ -106,3 +187,8 @@ class TestXfstestsView(unittest.TestCase):
self.assertIsNone(self.view.model)
self.view.model = self.model
self.assertEqual(self.view._testcaseview.model, self.model)
def test_filterbuttons(self):
"""Test the XfstestsView 'filterbuttons' property."""
self.assertEqual(self.view.filterbuttons,
self.view._testcaseview.filterbuttons)

View File

@ -24,6 +24,8 @@ class TestApplication(unittest.TestCase):
xfstestsdb.gtk.gsetup.APPLICATION_ID)
self.assertEqual(self.application.get_flags(),
Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
self.assertEqual(self.application.get_resource_base_path(),
xfstestsdb.gtk.gsetup.RESOURCE_PATH)
self.assertEqual(self.application.runid, 0)
self.assertIsNone(self.application.model)

View File

@ -26,6 +26,7 @@ class Application(Adw.Application):
def __init__(self, sql: sqlite.Connection):
"""Initialize the application."""
super().__init__(application_id=gsetup.APPLICATION_ID,
resource_base_path=gsetup.RESOURCE_PATH,
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
sql=sql)
@ -50,6 +51,8 @@ class Application(Adw.Application):
self.view = view.XfstestsView()
self.win = window.Window(child=self.view)
self.win.headerbar.pack_end(self.view.filterbuttons)
self.bind_property("runid", self.win, "runid")
self.bind_property("model", self.view, "model")
self.add_window(self.win)

View File

@ -9,11 +9,17 @@ gi.importlib.import_module("gi.repository.Adw")
DEBUG_STR = "-debug" if __debug__ else ""
APPLICATION_ID = f"com.nowheycreamery.xfstestsdb.gtk{DEBUG_STR}"
CSS_FILE = pathlib.Path(__file__).parent / "xfstestsdb.css"
__here = pathlib.Path(__file__).parent
CSS_FILE = __here / "xfstestsdb.css"
CSS_PRIORITY = gi.repository.Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
CSS_PROVIDER = gi.repository.Gtk.CssProvider()
CSS_PROVIDER.load_from_path(str(CSS_FILE))
RESOURCE_FILE = __here / "icons" / "xfstestsdb.gresource"
RESOURCE_PATH = "/com/nowheycreamery/xfstestsdb"
RESOURCE = gi.repository.Gio.Resource.load(str(RESOURCE_FILE))
gi.repository.Gio.resources_register(RESOURCE)
def add_style():
"""Add our custom stylesheet to the Gdk.Display."""

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 7.96875 1 c -3.851562 0 -6.96875 3.117188 -6.96875 6.96875 s 3.117188 6.96875 6.96875 6.96875 s 6.96875 -3.117188 6.96875 -6.96875 s -3.117188 -6.96875 -6.96875 -6.96875 z m -3 3.96875 h 1 h 0.03125 c 0.253906 0.011719 0.511719 0.128906 0.6875 0.3125 l 1.28125 1.28125 l 1.3125 -1.28125 c 0.265625 -0.230469 0.445312 -0.304688 0.6875 -0.3125 h 1 v 1 c 0 0.285156 -0.035156 0.550781 -0.25 0.75 l -1.28125 1.28125 l 1.25 1.25 c 0.1875 0.1875 0.28125 0.453125 0.28125 0.71875 v 1 h -1 c -0.265625 0 -0.53125 -0.09375 -0.71875 -0.28125 l -1.28125 -1.28125 l -1.28125 1.28125 c -0.1875 0.1875 -0.453125 0.28125 -0.71875 0.28125 h -1 v -1 c 0 -0.265625 0.09375 -0.53125 0.28125 -0.71875 l 1.28125 -1.25 l -1.28125 -1.28125 c -0.210938 -0.195312 -0.304688 -0.46875 -0.28125 -0.75 z m 0 0"/></svg>

After

Width:  |  Height:  |  Size: 927 B

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 8.234375 1.003906 c -2.15625 -0.070312 -4.277344 0.859375 -5.683594 2.601563 c -1.871093 2.316406 -2.066406 5.582031 -0.484375 8.105469 c 1.582032 2.527343 4.601563 3.777343 7.503906 3.109374 c 2.90625 -0.667968 5.074219 -3.117187 5.390626 -6.082031 c 0.046874 -0.359375 -0.101563 -0.71875 -0.394532 -0.933593 c -0.292968 -0.21875 -0.679687 -0.257813 -1.011718 -0.109376 c -0.332032 0.152344 -0.554688 0.472657 -0.582032 0.835938 c -0.226562 2.121094 -1.769531 3.863281 -3.851562 4.34375 c -2.078125 0.476562 -4.226563 -0.414062 -5.359375 -2.222656 c -1.132813 -1.808594 -0.992188 -4.128906 0.347656 -5.792969 s 3.578125 -2.289063 5.585937 -1.5625 c 0.339844 0.128906 0.71875 0.066406 0.996094 -0.167969 c 0.28125 -0.230468 0.410156 -0.59375 0.34375 -0.949218 c -0.0625 -0.355469 -0.316406 -0.648438 -0.660156 -0.761719 c -0.699219 -0.253907 -1.421875 -0.390625 -2.140625 -0.414063 z m 0 0"/><path d="m 13.167969 2.542969 l -5.292969 5.292969 l -2.417969 -2.417969 l -1.414062 1.414062 l 3.832031 3.832031 l 6.707031 -6.707031 z m 0 0"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 8 1 c -3.855469 0 -7 3.144531 -7 7 s 3.144531 7 7 7 s 7 -3.144531 7 -7 s -3.144531 -7 -7 -7 z m 0 2 c 2.753906 0 5 2.246094 5 5 s -2.246094 5 -5 5 s -5 -2.246094 -5 -5 s 2.246094 -5 5 -5 z m 0 0"/></svg>

After

Width:  |  Height:  |  Size: 341 B

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/nowheycreamery/xfstestsdb/icons/scalable/actions">
<file preprocess="xml-stripblanks">test-pass-symbolic.svg</file>
<file preprocess="xml-stripblanks">test-skip-symbolic.svg</file>
<file preprocess="xml-stripblanks">test-fail-symbolic.svg</file>
</gresource>
</gresources>

View File

@ -3,37 +3,80 @@
from gi.repository import GObject
from gi.repository import Gtk
from .model import TestCaseList
from .model import TestCaseFilter
from . import button
from . import row
class FilterButtons(Gtk.Box):
"""Buttons for controlling the TestCaseFilter."""
passed = GObject.Property(type=bool, default=False)
skipped = GObject.Property(type=bool, default=False)
failure = GObject.Property(type=bool, default=True)
def __init__(self):
"""Initialize the FilterButtons."""
super().__init__()
self.add_css_class("linked")
self._passed = button.StatusToggle("test-pass", "passed")
self._skipped = button.StatusToggle("test-skip", "skipped")
self._failure = button.StatusToggle("test-fail", "failure",
active=True)
self._passed.bind_property("active", self, "passed")
self._skipped.bind_property("active", self, "skipped")
self._failure.bind_property("active", self, "failure")
self.append(self._passed)
self.append(self._skipped)
self.append(self._failure)
class TestCaseView(Gtk.ScrolledWindow):
"""Displays our TestCaseList model to the user."""
filterbuttons = GObject.Property(type=FilterButtons)
def __init__(self):
"""Initialize a TestCaseView."""
super().__init__(child=Gtk.ColumnView(model=Gtk.NoSelection(),
show_row_separators=True,
show_column_separators=True,
hexpand=True, vexpand=True))
hexpand=True, vexpand=True),
filterbuttons=FilterButtons())
self._testcase = Gtk.ColumnViewColumn(title="testcase",
factory=row.LabelFactory("name"))
self._testfilter = TestCaseFilter()
self._filtermodel = Gtk.FilterListModel(filter=self._testfilter)
for prop in ["passed", "skipped", "failure"]:
self.filterbuttons.bind_property(prop, self._testfilter, prop)
self.props.child.get_model().set_model(self._filtermodel)
self.add_css_class("card")
def __xunit_column(self, xunit: str) -> None:
return Gtk.ColumnViewColumn(title=xunit, expand=True,
factory=row.ResultFactory(xunit))
def make_buttons(self) -> FilterButtons:
"""Make a new FilterButtons instance connected to this View."""
return FilterButtons()
@GObject.Property(type=TestCaseList)
def model(self) -> TestCaseList:
"""Get the TestCaseList shown by the View."""
return self.props.child.get_model().get_model()
return self._filtermodel.props.model
@model.setter
def model(self, new: TestCaseList) -> None:
for col in [col for col in self.props.child.get_columns()]:
self.props.child.remove_column(col)
self.props.child.get_model().set_model(new)
self._filtermodel.props.model = new
if new is not None:
self.props.child.append_column(self._testcase)
@ -54,3 +97,8 @@ class XfstestsView(Gtk.Box):
self.bind_property("model", self._testcaseview, "model")
self.append(self._testcaseview)
@GObject.Property(type=FilterButtons)
def filterbuttons(self) -> FilterButtons:
"""Get the FilterButtons attached to the child TestCaseView."""
return self._testcaseview.filterbuttons

View File

@ -1,15 +1,27 @@
/* Copyright 2023 (c) Anna Schumaker. */
button.passed > image {
color: @success_color;
}
cell.passed {
color: @success_fg_color;
background-color: @success_bg_color;
}
button.skipped > image {
color: @warning_color;
}
cell.skipped {
color: @warning_fg_color;
background-color: @warning_bg_color;
}
button.failure > image {
color: @error_color;
}
cell.failure {
color: @error_fg_color;
background-color: @error_bg_color;